Compare commits

..

81 Commits

Author SHA1 Message Date
q
ef65d0e095 Merge remote-tracking branch 'origin/main' 2025-09-10 17:10:52 +08:00
q
6438505f4a icloud国际版支持,QQ邮箱文件信息获取 2025-09-10 17:10:39 +08:00
qaiu
1be5030dd1 添加docker多平台支持 2025-09-10 17:06:40 +08:00
q
421b2f4a42 Merge remote-tracking branch 'origin/main' 2025-08-19 18:57:00 +08:00
q
a66bf84381 直链API添加文件信息
修复蓝奏目录文件大小处理报错问题 #120
2025-08-19 18:56:42 +08:00
qaiu
0c4d366d6d 更新 README.md 2025-08-14 19:36:09 +08:00
qaiu
a1d0a921fa 更新 README.md 2025-08-14 19:28:44 +08:00
q
2092230a61 更新文档 2025-08-14 15:27:03 +08:00
q
6e5ae6eff3 Merge remote-tracking branch 'origin/main' 2025-08-12 13:32:09 +08:00
q
4f8259d772 1. iz match fixed
2. redirect res content add "text/html; charset=utf-8"
2025-08-12 13:29:59 +08:00
qaiu
8b987d9824 Update README.md 2025-08-11 13:32:33 +08:00
q
e8ba451d18 build status 2025-08-11 13:26:06 +08:00
q
77758db463 Merge remote-tracking branch 'origin/main' 2025-08-11 13:23:59 +08:00
q
6c58598a8e 用户API 2025-08-11 13:23:30 +08:00
qaiu
3ac35230a3 Update README.md 2025-08-11 13:18:00 +08:00
q
ca91302d28 Merge remote-tracking branch 'origin/main' 2025-08-11 13:14:59 +08:00
q
e07272a5dc 添加支持 QQ闪传,微雨云,优化前端逻辑 2025-08-11 13:14:43 +08:00
qaiu
461305e1df Update README.md 2025-08-08 12:33:46 +08:00
q
8e8ab10a0f Merge remote-tracking branch 'origin/main' 2025-08-05 15:50:50 +08:00
q
e754326925 fixed p118 link 2025-08-05 15:50:32 +08:00
qaiu
4c92994c6f 更新 README.md 2025-08-05 13:14:09 +08:00
qaiu
66c57f47ac Merge pull request #113 from qaiu/dependabot/maven/org.apache.commons-commons-lang3-3.18.0
Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0
2025-07-30 09:17:42 +08:00
dependabot[bot]
ec689eadd8 Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0
Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 09:05:14 +00:00
qaiu
c1e15709a7 Merge pull request #110 from qaiu/dependabot/maven/core-database/org.apache.commons-commons-lang3-3.18.0
Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0 in /core-database
2025-07-21 17:02:12 +08:00
q
2848937ce7 Merge remote-tracking branch 'origin/main' 2025-07-18 13:00:33 +08:00
q
42ff0c21b2 1. 默认缓存时间修改
2. 文件夹解析异常处理
3. 首页优化
2025-07-18 13:00:12 +08:00
qaiu
3ed7e547e6 更新 README.md
联系方式更新
2025-07-16 13:30:47 +08:00
q
fad8e688df 首页样式优化 2025-07-15 18:09:11 +08:00
q
b2f2dcac4c Merge remote-tracking branch 'origin/main' 2025-07-14 16:16:13 +08:00
q
fcba78e977 启动参数优化 2025-07-14 16:16:00 +08:00
qaiu
77c9d777a1 更新 README.md 2025-07-12 13:47:03 +08:00
dependabot[bot]
4460659210 Bump org.apache.commons:commons-lang3 in /core-database
Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-12 01:16:21 +00:00
q
8631524107 移动端布局优化 2025-07-11 11:51:08 +08:00
q
0579588814 优化构建流v0.1.9b6b 2025-07-10 19:17:22 +08:00
q
df2bfb6ac7 优化构建流 2025-07-10 19:12:15 +08:00
q
517b6f8910 del 2025-07-10 19:07:02 +08:00
q
94a46d2833 目录解析支持优化 v0.1.9b6 2025-07-10 18:59:59 +08:00
q
1631a0faa1 目录解析支持优化 v0.1.9b5 2025-07-10 18:58:12 +08:00
qaiu
06d5943cb6 Merge remote-tracking branch 'origin/main' 2025-07-09 07:58:10 +08:00
qaiu
3095e13676 ye目录解析 2025-07-09 07:57:47 +08:00
qaiu
482cbce7e8 Update maven.yml 2025-07-09 07:09:58 +08:00
qaiu
ef2fc3ab98 lz目录解析预览 2025-07-09 07:06:12 +08:00
q
5b57b05eae 目录解析支持优化 v0.1.9b2 2025-07-08 18:58:38 +08:00
q
093579c6f5 Merge remote-tracking branch 'origin/main' 2025-07-08 18:57:17 +08:00
q
c2d4990d7f 目录解析支持优化 v0.1.9b2 2025-07-08 18:55:19 +08:00
qaiu
40e8380738 更新 README.md 2025-07-08 04:03:42 +08:00
qaiu
b716e1e861 更新 README.md 2025-07-08 04:02:50 +08:00
qaiu
8432d4952c 更新 README.md 2025-07-08 03:51:46 +08:00
qaiu
dd8f085f63 更新 README.md 2025-07-08 03:51:00 +08:00
qaiu
161ff8d8a3 更新 README.md 2025-07-08 03:48:19 +08:00
qaiu
1390cd0104 更新 README.md 2025-07-08 03:47:09 +08:00
qaiu
7a02b1e97f 更新 README.md 2025-07-08 03:46:19 +08:00
qaiu
036f107c90 更新 README.md
docker镜像更新
2025-07-08 03:20:18 +08:00
qaiu
5652383450 Update README.md 2025-07-08 02:23:01 +08:00
qaiu
9a047a5da0 更新 README.md 2025-07-04 19:38:18 +08:00
qaiu
8975743a37 更新 README.md 2025-07-04 19:34:53 +08:00
q
0e30eafe49 目录解析支持 2025-07-04 19:20:06 +08:00
q
7facb62f21 Merge remote-tracking branch 'origin/main' 2025-07-04 19:17:35 +08:00
q
30d43cb961 目录解析支持 2025-07-04 19:16:36 +08:00
q
c505b17e35 目录解析支持 2025-07-04 19:11:39 +08:00
qaiu
080c4c753d Create update-release-badge.yml 2025-07-04 09:34:22 +08:00
q
ade0d34d91 start 2025-07-02 18:40:32 +08:00
qaiu
56d082eb0b Update maven.yml
提交任意tag 触发工作流
2025-07-02 18:36:45 +08:00
qaiu
795c4529ba Update AppMain.java 2025-07-02 18:29:46 +08:00
qaiu
0f5cfe22ea Update maven.yml
构建docker添加tag版本
2025-07-02 18:26:48 +08:00
qaiu
925ad2c3a5 Update AppMain.java 2025-07-02 18:20:30 +08:00
qaiu
f3e96907fe Update maven.yml
docker镜像构建添加版本号
2025-07-02 18:19:01 +08:00
qaiu
75a1e58a7d Update AppMain.java 2025-07-02 18:13:19 +08:00
yyzy-official
379e889f71 Update maven.yml
修改工作流版本号问题
2025-07-02 18:09:48 +08:00
yyzy-official
40c06f397b Update README.md
收款码隐藏
2025-07-02 17:41:23 +08:00
yyzy-official
9e9302436e Update README.md
1. 微信联系方式
2. 预览地址
2025-07-02 17:39:14 +08:00
qaiu
6d816d4193 Update README.md 2025-07-02 15:50:29 +08:00
qaiu
438eda9c08 Update README.md 2025-07-02 15:46:55 +08:00
q
ace39e4633 jdk24适配 2025-06-17 15:48:52 +08:00
qaiu
7712391f29 更新 README.md 2025-06-06 07:17:49 +08:00
qaiu
65f08dcb02 Update README.md 2025-06-04 16:28:50 +08:00
qaiu
1d332aa6f4 Update README.md 2025-06-04 16:26:56 +08:00
QAIU
ba81641517 1. 修改docker构建失败 2025-06-04 15:37:50 +08:00
qaiu
fb30bdb879 Update README.md 2025-06-04 15:27:21 +08:00
qaiu
fc451d3b41 Update README.md 文件夹解析说明 2025-06-04 15:25:26 +08:00
QAIU
ffee1f3462 1. 修复 小飞机解析错误#106
2. 添加 123云盘文件夹分享下解析为压缩包下载直链
3. 添加 123云盘全部域名支持: "www.123pan.com","www.123pan.cn","www.123865.com","www.123684.com","www.123912.com","www.123pan.cn"
2025-06-04 15:13:05 +08:00
69 changed files with 5783 additions and 776 deletions

View File

@@ -15,7 +15,10 @@ permissions:
on:
push:
branches: [ "main" ]
tags:
- '*' # 只有推送tag时才会触发构建
branches-ignore:
- '*' # 排除所有分支的提交
paths-ignore:
- 'bin/**'
- '.github/**'
@@ -25,7 +28,8 @@ on:
- '*.txt'
- '*.md'
pull_request:
branches: [ "main" ]
branches:
- "main"
jobs:
build:
@@ -76,12 +80,22 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract git tag
id: tag
run: |
GIT_TAG=$(git tag --points-at HEAD | head -n 1)
echo "tag=$GIT_TAG" >> $GITHUB_OUTPUT
- name: Build and push Docker image
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:main
ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
ghcr.io/qaiu/netdisk-fast-download:latest

114
README.md
View File

@@ -1,8 +1,9 @@
<div align=center><img src="https://bd2.qaiu.cn/blog/lanzou111.403f7881.png" height="160" width="160"></div>
<p align="center">
<img src="https://github.com/user-attachments/assets/87401aae-b0b6-4ffb-bbeb-44756404d26f" alt="项目预览图" />
</p>
<p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml/badge.svg?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://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.6-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>
@@ -10,12 +11,36 @@
</p>
# netdisk-fast-download 网盘分享链接云解析服务
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享。
预览地址 https://lz.qaiu.top
预览地址2(可以解析onedrive) http://8.209.249.88:6402
# netdisk-fast-download 网盘分享链接云解析服务
QQ群1017480890
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。
## 快速开始
命令行下载分享文件:
```shell
curl -LOJ "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
```
或者使用wget:
```shell
wget -O bilibili.mp4 "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
```
或者使用浏览器[直接访问](https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4):
```
### 调用演示站下载:
https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234
### 调用演示站预览:
https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4
```
## 预览地址
[预览地址1](https://lz.qaiu.top)
[预览地址2](http://www.722shop.top:6401)
[天翼云盘大文件解析限时开放](https://189.qaiu.top)
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
**小飞机解析有IP限制多数云服务商的大陆IP会被拦截可以自行配置代理和本程序无关**
@@ -39,7 +64,8 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
- [118网盘(已停服)-p118](https://www.118pan.com/)
- [文叔叔-ws](https://www.wenshushu.cn/)
- [联想乐云-le](https://lecloud.lenovo.com/)
- [QQ邮箱文件中转站-qq](https://mail.qq.com/)
- [QQ邮箱云盘-qqw](https://mail.qq.com/)
- [QQ闪传-qqsc](https://nutty.qq.com/nutty/ssr/26797.html)
- [城通网盘-ct](https://www.ctfile.com)
- [网易云音乐分享链接-mnes](https://music.163.com)
- [酷狗音乐分享链接-mkgs](https://www.kugou.com)
@@ -58,7 +84,7 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
- [联通云盘-pwo](https://pan.wo.cn/)
- [天翼云盘-p189](https://cloud.189.cn/)
### API接口说明
## API接口说明
your_host指的是您的域名或者IP实际使用时替换为实际域名或者IP端口默认6400可以使用nginx代理来做域名访问。
解析方式分为两种类型直接跳转下载文件和获取下载链接,
每一种都提供了两种接口形式: `通用接口parser?url=``网盘标志/分享key拼接的短地址标志短链`,所有规则参考示例。
@@ -72,21 +98,28 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
API规则:
> 建议使用UrlEncode编码分享链接
```
1. 解析并自动302跳转
http://your_host/parser?url=分享链接&pwd=xxx
或者 http://your_host/parser?url=UrlEncode(分享链接)&pwd=xxx
http://your_host/parser?url=分享链接&pwd=xxx
http://your_host/parser?url=UrlEncode(分享链接)&pwd=xxx
http://your_host/d/网盘标识/分享key@分享密码
2. 获取解析后的直链--JSON格式
http://your_host/json/parser?url=分享链接&pwd=xxx
http://your_host/json/parser?url=分享链接&pwd=xxx
http://your_host/json/网盘标识/分享key@分享密码
```
3. 文件夹解析v0.1.8fixed3新增
http://your_host/json/getFileList?url=分享链接&pwd=xxx
### json接口说明
#### 1. 文件解析:/json/parser?url=分享链接&pwd=xxx
json返回数据格式示例:
`shareKey`: 全局分享key
`directLink`: 下载链接
`cacheHit`: 是否为缓存链接
`expires`: 缓存到期时间
```json
{
"code": 200,
@@ -103,7 +136,7 @@ json返回数据格式示例:
"timestamp": 1726637151902
}
```
2. 分享链接详情接口 /v2/linkInfo?url=分享链接
#### 2. 分享链接详情接口 /v2/linkInfo?url=分享链接
```json
{
"code": 200,
@@ -132,7 +165,36 @@ json返回数据格式示例:
"timestamp": 1736489219402
}
```
3. 解析次数统计接口 /v2/statisticsInfo
#### 3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
/v2/getFileList?url=分享链接&pwd=分享密码
```json
{
"code": 200,
"msg": "success",
"success": true,
"data": [
{
"fileName": "xxx",
"fileId": "xxx",
"fileIcon": null,
"size": 999,
"sizeStr": "999 M",
"fileType": "file/folder",
"filePath": null,
"createTime": "17 小时前",
"updateTime": null,
"createBy": null,
"description": null,
"downloadCount": "下载次数",
"panType": "lz",
"parserUrl": "下载链接/文件夹链接",
"extParameters": null
}
]
}
```
#### 4. 解析次数统计接口 /v2/statisticsInfo
```json
{
"code": 200,
@@ -227,15 +289,15 @@ mkdir -p netdisk-fast-download
cd netdisk-fast-download
# 拉取镜像
docker pull ghcr.io/qaiu/netdisk-fast-download:main
docker pull ghcr.io/qaiu/netdisk-fast-download:latest
# 复制配置文件或下载仓库web-service\src\main\resources
docker create --name netdisk-fast-download ghcr.io/qaiu/netdisk-fast-download:main
docker create --name netdisk-fast-download ghcr.io/qaiu/netdisk-fast-download:latest
docker cp netdisk-fast-download:/app/resources ./resources
docker rm netdisk-fast-download
# 启动容器
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.io/qaiu/netdisk-fast-download:main
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.io/qaiu/netdisk-fast-download:latest
# 反代6401端口
@@ -250,15 +312,15 @@ mkdir -p netdisk-fast-download
cd netdisk-fast-download
# 拉取镜像
docker pull ghcr.nju.edu.cn/qaiu/netdisk-fast-download:main
docker pull ghcr.nju.edu.cn/qaiu/netdisk-fast-download:latest
# 复制配置文件或下载仓库web-service\src\main\resources
docker create --name netdisk-fast-download ghcr.nju.edu.cn/qaiu/netdisk-fast-download:main
docker create --name netdisk-fast-download ghcr.nju.edu.cn/qaiu/netdisk-fast-download:latest
docker cp netdisk-fast-download:/app/resources ./resources
docker rm netdisk-fast-download
# 启动容器
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.nju.edu.cn/qaiu/netdisk-fast-download:main
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.nju.edu.cn/qaiu/netdisk-fast-download:latest
# 反代6401端口
@@ -348,13 +410,19 @@ Core模块集成Vert.x实现类似spring的注解式路由API
## 支持该项目
开源不易,用爱发电,本项目长期维护如果觉得有帮助, 可以请作者喝杯咖啡, 感谢支持
### 关于专属版
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联调云盘的解析支持
199元, 包含部署服务和首页定制, 需提供宝塔环境
可以提供功能定制开发, 加v价格详谈: qaiu-cn
可以提供功能定制开发, 加v价格详谈:
<p>qq: 197575894</p>
<p>wechat: imcoding_</p>
<!--
![image](https://github.com/qaiu/netdisk-fast-download/assets/29825328/54276aee-cc3f-4ebd-8973-2e15f6295819)
[手机端支付宝打赏跳转链接](https://qr.alipay.com/fkx01882dnoxxtjenhlxt53)
-->

View File

@@ -1,22 +0,0 @@
@echo off
setlocal
rem 获取当前 Java 版本信息并搜索是否包含 "17."
java -version 2>&1 | find "17." >nul
rem 如果找不到 JDK 17.x则下载并安装
if errorlevel 1 (
echo JDK 17.x not found. Downloading and installing...
REM 这里添加下载和安装 JDK 的代码
rem 验证安装
java -version
echo JDK 17.x installation complete.
) else (
echo JDK 17.x is already installed.
)
endlocal
pause

86
bin/nfd-install.sh Normal file
View File

@@ -0,0 +1,86 @@
#!/bin/bash
set -e
# ----------- 配置区域 ------------
# JRE 下载目录
JRE_DIR="/opt/custom-jre17"
# 使用阿里云镜像下载 JREOpenJDK 17
JRE_TARBALL_URL="https://mirrors.tuna.tsinghua.edu.cn/Adoptium/17/jre/x64/linux/OpenJDK17U-jre_x64_linux_hotspot_17.0.15_6.tar.gz"
# ZIP 文件下载相关
ZIP_URL="http://www.722shop.top:6401/parser?url="
ZIP_DEST_DIR="/opt/target-zip"
ZIP_FILE_NAME="nfd.zip"
# --------------------------------
# 创建目录
mkdir -p "$JRE_DIR"
mkdir -p "$ZIP_DEST_DIR"
# -------- 检查 unzip 是否存在 --------
if ! command -v unzip >/dev/null 2>&1; then
echo "unzip 未安装,正在安装..."
if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y unzip
elif command -v yum >/dev/null 2>&1; then
yum install -y unzip
elif command -v dnf >/dev/null 2>&1; then
dnf install -y unzip
else
echo "不支持的包管理器,无法自动安装 unzip请手动安装后重试。"
exit 1
fi
else
echo "unzip 已安装"
fi
# -------- 下载并解压 JRE --------
echo "下载 JRE 17 到 $JRE_DIR..."
curl -L "$JRE_TARBALL_URL" -o "$JRE_DIR/jre17.tar.gz"
echo "解压 JRE..."
tar -xzf "$JRE_DIR/jre17.tar.gz" -C "$JRE_DIR" --strip-components=1
rm "$JRE_DIR/jre17.tar.gz"
echo "JRE 解压完成"
# -------- 下载 ZIP 文件 --------
ZIP_PATH="$ZIP_DEST_DIR/$ZIP_FILE_NAME"
echo "下载 ZIP 文件到 $ZIP_PATH..."
curl -L "$ZIP_URL" -o "$ZIP_PATH"
# -------- 解压 ZIP 文件 --------
echo "解压 ZIP 文件到 $ZIP_DEST_DIR..."
unzip -o "$ZIP_PATH" -d "$ZIP_DEST_DIR"
echo "解压完成"
# -------- 启动 JAR 程序 --------
echo "进入 JAR 目录并后台运行程序..."
JAR_DIR="/opt/target-zip/netdisk-fast-download"
JAR_FILE="netdisk-fast-download.jar"
JAVA_BIN="$JRE_DIR/bin/java"
LOG_FILE="$JAR_DIR/app.log"
if [ ! -d "$JAR_DIR" ]; then
echo "[错误] 找不到 JAR 目录: $JAR_DIR"
exit 1
fi
cd "$JAR_DIR"
if [ ! -f "$JAR_FILE" ]; then
echo "[错误] 找不到 JAR 文件: $JAR_FILE"
exit 1
fi
if [ ! -x "$JAVA_BIN" ]; then
echo "[错误] 找不到可执行的 java: $JAVA_BIN"
exit 1
fi
# 后台运行,日志记录
nohup "$JAVA_BIN" -jar "$JAR_FILE" > "$LOG_FILE" 2>&1 &
echo "程序已在后台启动 ✅"
echo "日志路径: $LOG_FILE"

View File

@@ -41,7 +41,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
<version>3.18.0</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>

View File

@@ -54,9 +54,9 @@ public final class Deploy {
public void start(String[] args, Handler<JsonObject> handle) {
this.mainThread = Thread.currentThread();
this.handle = handle;
if (args.length > 0) {
if (args.length > 0 && args[0].startsWith("app-")) {
// 启动参数dev或者prod
path.append("-").append(args[0]);
path.append("-").append(args[0].replace("app-",""));
}
// 读取yml配置

View File

@@ -16,17 +16,24 @@ public interface BeforeInterceptor extends Handler<RoutingContext> {
default Handler<RoutingContext> doHandle() {
return ctx -> {
ctx.put(IS_NEXT, false);
BeforeInterceptor.this.handle(ctx);
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
sendError(ctx, 403);
// 加同步锁
synchronized (BeforeInterceptor.class) {
ctx.put(IS_NEXT, false);
BeforeInterceptor.this.handle(ctx);
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
sendError(ctx, 403);
}
}
};
}
default void doNext(RoutingContext context) {
context.put(IS_NEXT, true);
context.next();
// 设置上下文状态为可以继续执行
// 添加同步锁保障多线程下执行时序
synchronized (BeforeInterceptor.class) {
context.put(IS_NEXT, true);
context.next();
}
}
void handle(RoutingContext context);

View File

@@ -8,7 +8,11 @@ public interface ConfigConstant {
String SERVER = "server";
String CACHE = "cache";
String PROXY_SERVER = "proxy-server";
String PROXY = "proxy";
String AUTHS = "auths";
String GLOBAL_CONFIG = "globalConfig";
String CUSTOM_CONFIG = "customConfig";
String ASYNC_SERVICE_INSTANCES = "asyncServiceInstances";

View File

@@ -12,7 +12,8 @@ import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
public class ResponseUtil {
public static void redirect(HttpServerResponse response, String url) {
response.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
response.putHeader(CONTENT_TYPE, "text/html; charset=utf-8")
.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
}
public static void redirect(HttpServerResponse response, String url, Promise<?> promise) {

View File

@@ -11,7 +11,7 @@
<artifactId>parser</artifactId>
<packaging>jar</packaging>
<name>${project.groupId}:${project.artifactId}</name>
<name>cn.qaiu:parser</name>
<description>NFD parser</description>
<url>https://qaiu.top</url>

View File

@@ -69,6 +69,12 @@ public class FileInfo {
*/
private String parserUrl;
//预览地址
private String previewUrl;
// 文件hash默认类型为md5
private String hash;
/**
* 扩展参数
*/
@@ -199,6 +205,22 @@ public class FileInfo {
this.parserUrl = parserUrl;
return this;
}
public String getPreviewUrl() {
return previewUrl;
}
public FileInfo setPreviewUrl(String previewUrl) {
this.previewUrl = previewUrl;
return this;
}
public String getHash() {
return hash;
}
public FileInfo setHash(String hash) {
this.hash = hash;
return this;
}
public Map<String, Object> getExtParameters() {
return extParameters;

View File

@@ -15,6 +15,7 @@ public class ShareLinkInfo {
/**
* 其他参数预定义
* dirId: 目录ID 传入
* auths: 认证相关 传入
* UA: 浏览器请求头 传入
* fileInfo: 解析成功的文件信息对象 传出

View File

@@ -19,7 +19,7 @@ public interface IPanTool {
*/
default Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
promise.complete();
promise.fail("Not implemented yet");
return promise.future();
}
@@ -29,7 +29,7 @@ public interface IPanTool {
*/
default Future<String> parseById() {
Promise<String> promise = Promise.promise();
promise.complete();
promise.complete("Not implemented yet");
return promise.future();
}
}

View File

@@ -116,12 +116,21 @@ public abstract class PanBase implements IPanTool {
*/
protected void fail(Throwable t, String errorMsg, Object... args) {
try {
// 判断是否已经完成
if (promise.future().isComplete()) {
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
return;
}
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
log.error("解析异常: " + s, t.fillInStackTrace());
promise.fail(baseMsg() + ": 解析异常: " + s + " -> " + t);
} catch (Exception e) {
log.error("ErrorMsg format fail. The parameter has been discarded", e);
log.error("解析异常: " + errorMsg, t.fillInStackTrace());
if (promise.future().isComplete()) {
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
return;
}
promise.fail(baseMsg() + ": 解析异常: " + errorMsg + " -> " + t);
}
}
@@ -134,9 +143,18 @@ public abstract class PanBase implements IPanTool {
*/
protected void fail(String errorMsg, Object... args) {
try {
// 判断是否已经完成
if (promise.future().isComplete()) {
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
return;
}
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
promise.fail(baseMsg() + " - 解析异常: " + s);
} catch (Exception e) {
if (promise.future().isComplete()) {
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
return;
}
log.error("ErrorMsg format fail. The parameter has been discarded", e);
promise.fail(baseMsg() + " - 解析异常: " + errorMsg);
}
@@ -184,17 +202,18 @@ public abstract class PanBase implements IPanTool {
try {
log.error(decompressGzip((Buffer) res.body()));
fail(decompressGzip((Buffer) res.body()));
throw new RuntimeException("响应不是JSON格式");
//throw new RuntimeException("响应不是JSON格式");
} catch (IOException ex) {
log.error("响应gzip解压失败");
fail("响应gzip解压失败: {}", ex.getMessage());
throw new RuntimeException("响应gzip解压失败", ex);
//throw new RuntimeException("响应gzip解压失败", ex);
}
} else {
log.error("解析失败: json格式异常: {}", res.bodyAsString());
fail("解析失败: json格式异常: {}", res.bodyAsString());
throw new RuntimeException("解析失败: json格式异常");
//throw new RuntimeException("解析失败: json格式异常");
}
return JsonObject.of();
}
}
@@ -215,8 +234,9 @@ public abstract class PanBase implements IPanTool {
}
} catch (Exception e) {
fail("解析失败: res格式异常");
throw new RuntimeException("解析失败: res格式异常");
//throw new RuntimeException("解析失败: res格式异常");
}
return null;
}
protected void complete(String url) {

View File

@@ -23,9 +23,87 @@ import static java.util.regex.Pattern.compile;
public enum PanDomainTemplate {
// https://www.ilanzou.com/s/
IZ("蓝奏云优享",
compile("https://www\\.ilanzou\\.com/s/(?<KEY>.+)"),
"https://www.ilanzou.com/s/{shareKey}",
IzTool.class),
// 网盘定义
/*
lanzoul.com
lanzouh.com
lanosso.com
lanpv.com
bakstotre.com
lanzouo.com
lanzov.com
lanpw.com
ulanzou.com
lanzouf.com
lanzn.com
lanzouj.com
lanzouk.com
lanzouq.com
lanzouv.com
lanzoue.com
lanzouw.com
lanzoub.com
lanzouu.com
lanwp.com
lanzouy.com
lanzoup.com
woozooo.com
lanzv.com
dmpdmp.com
lanrar.com
webgetstore.com
lanzb.com
lanzoux.com
lanzout.com
lanzouc.com
ilanzou.com
lanzoui.com
lanzoug.com
lanzoum.com
t-is.cn
*/
LZ("蓝奏云",
compile("https://(?:[a-zA-Z\\d-]+\\.)?((lanzou[a-z])|(lanzn))\\.com/(.+/)?(?<KEY>.+)"),
compile("https://(?:[a-zA-Z\\d-]+\\.)?(" +
"lanzoul|" +
"lanzouh|" +
"lanosso|" +
"lanpv|" +
"bakstotre|" +
"lanzouo|" +
"lanzov|" +
"lanpw|" +
"ulanzou|" +
"lanzouf|" +
"lanzn|" +
"lanzouj|" +
"lanzouk|" +
"lanzouq|" +
"lanzouv|" +
"lanzoue|" +
"lanzouw|" +
"lanzoub|" +
"lanzouu|" +
"lanwp|" +
"lanzouy|" +
"lanzoup|" +
"woozooo|" +
"lanzv|" +
"dmpdmp|" +
"lanrar|" +
"webgetstore|" +
"lanzb|" +
"lanzoux|" +
"lanzout|" +
"lanzouc|" +
"lanzoui|" +
"lanzoug|" +
"lanzoum" +
")\\.com/(.+/)?(?<KEY>.+)"),
"https://lanzoux.com/{shareKey}",
LzTool.class),
@@ -48,11 +126,6 @@ public enum PanDomainTemplate {
"https://v2.fangcloud.com/s/{shareKey}",
"https://www.fangcloud.com/",
FcTool.class),
// https://www.ilanzou.com/s/
IZ("蓝奏云优享",
compile("https://www\\.ilanzou\\.com/s/(?<KEY>.+)"),
"https://www.ilanzou.com/s/{shareKey}",
IzTool.class),
// https://wx.mail.qq.com/ftn/download?
QQ("QQ邮箱中转站",
compile("https://i?wx\\.mail\\.qq\\.com/ftn/download\\?(?<KEY>.+)"),
@@ -65,14 +138,68 @@ public enum PanDomainTemplate {
"https://wx.mail.qq.com/s?k={shareKey}",
"https://mail.qq.com",
QQwTool.class),
// https://qfile.qq.com/q/xxx
QQSC("QQ闪传",
compile("https://qfile\\.qq\\.com/q/(?<KEY>.+)"),
"https://qfile.qq.com/q/{shareKey}",
QQscTool.class),
// https://f.ws59.cn/f/或者https://www.wenshushu.cn/f/
WS("文叔叔",
compile("https://(f\\.ws(\\d{2})\\.cn|www\\.wenshushu\\.cn)/f/(?<KEY>.+)"),
"https://www.wenshushu.cn/f/{shareKey}",
WsTool.class),
// https://www.123pan.com/s/
/*
123254.com
123957.com
123295.com
123panpay.com
123860.com
123pan.com
123245.com
123278.com
123842.com
123294.com
123865.com
123773.com
123624.com
123684.com
123641.com
123259.com
123912.com
123952.com
123652.com
123pan.cn
123635.com
123242.com
123795.com
*/
YE("123网盘",
compile("https://www\\.(123pan|123865|123684)\\.com/s/(?<KEY>.+)(.html)?"),
compile("https://www\\.(" +
"123254\\.com|" +
"123957\\.com|" +
"123295\\.com|" +
"123panpay\\.com|" +
"123860\\.com|" +
"123pan\\.com|" +
"123245\\.com|" +
"123278\\.com|" +
"123842\\.com|" +
"123294\\.com|" +
"123865\\.com|" +
"123773\\.com|" +
"123624\\.com|" +
"123684\\.com|" +
"123641\\.com|" +
"123259\\.com|" +
"123912\\.com|" +
"123952\\.com|" +
"123652\\.com|" +
"123pan\\.cn|" +
"123635\\.com|" +
"123242\\.com|" +
"123795\\.com" +
")/s/(?<KEY>.+)(.html)?"),
"https://www.123pan.com/s/{shareKey}",
YeTool.class),
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
@@ -116,7 +243,7 @@ public enum PanDomainTemplate {
PgdTool.class),
// iCloud https://www.icloud.com.cn/iclouddrive/xxx#fonts
PIC("iCloud",
compile("https://www\\.icloud\\.com\\.cn/iclouddrive/(?<KEY>[a-z_A-Z\\d-=]+)(#(.+))?"),
compile("https://www\\.icloud\\.com(\\.cn)?/iclouddrive/(?<KEY>[a-z_A-Z\\d-=]+)(#(.+))?"),
"https://www.icloud.com.cn/iclouddrive/{shareKey}",
PicTool.class),
// https://www.dropbox.com/scl/fi/cwnbms1yn8u6rcatzyta7/emqx-5.0.26-el7-amd64.tar.gz?rlkey=3uoi4bxz5mv93jmlaws0nlol1&e=8&st=fe0lclc2&dl=0
@@ -143,7 +270,7 @@ public enum PanDomainTemplate {
// http://163cn.tv/xxx
MNES("网易云音乐分享",
compile("http(s)?://163cn\\.tv/(?<KEY>.+)"),
"http://163cn.tv/{shareKey}",
"https://163cn.tv/{shareKey}",
MnesTool.class),
// https://music.163.com/#/song?id=xxx
MNE("网易云音乐歌曲详情",

View File

@@ -14,16 +14,11 @@ 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 org.apache.commons.lang3.StringUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.zip.GZIPInputStream;
/**
* 小飞机网盘
@@ -95,6 +90,9 @@ public class FjTool extends PanBase {
final String shareId = shareLinkInfo.getShareKey();
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
@@ -102,50 +100,32 @@ public class FjTool extends PanBase {
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
client.postAbs(UriTemplate.of(FIRST_REQUEST_URL))
client.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(res -> {
// 处理GZ压缩
// 使用GZIPInputStream来解压数据
String decompressedString;
try (ByteArrayInputStream bais = new ByteArrayInputStream(res.body().getBytes());
GZIPInputStream gzis = new GZIPInputStream(bais);
BufferedReader reader = new BufferedReader(new InputStreamReader(gzis, StandardCharsets.UTF_8))) {
// 用于存储解压后的字符串
StringBuilder decompressedData = new StringBuilder();
// 逐行读取解压后的数据
String line;
while ((line = reader.readLine()) != null) {
decompressedData.append(line);
}
// 此时decompressedData.toString()包含了解压后的字符串
decompressedString = decompressedData.toString();
} catch (IOException e) {
// 处理可能的IO异常
fail(FIRST_REQUEST_URL + " 响应异常");
return;
}
// 处理GZ压缩结束
JsonObject resJson = new JsonObject(decompressedString);
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 200) {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (resJson.getJsonArray("list").size() == 0) {
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
// 如果是目录返回目录ID
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
return;
}
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
if (fileList.getInteger("fileType") == 2) {
promise.complete(fileList.getInteger("folderId").toString());
@@ -169,7 +149,7 @@ public class FjTool extends PanBase {
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", shareId);
System.out.println(httpRequest.toString());
// System.out.println(httpRequest.toString());
httpRequest.send().onSuccess(res2 -> {
MultiMap headers = res2.headers();
if (!headers.contains("Location")) {
@@ -190,104 +170,125 @@ public class FjTool extends PanBase {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
/*
{
"iconId" : 13,
"fileName" : "酷我音乐车机版 6.4.2.20.apk",
"fileSaves" : 52,
"fileStars" : 5.0,
"type" : 1,
"userId" : 1392902,
"fileComments" : 0,
"fileSize" : 68854,
"fileIcon" : "https://d.feijix.com/storage/files/icon/2024/06/08/7/8146637/6534494874910391.gz?t=67a5ea7c&rlimit=20&us=nMfuftjBN5&sign=f72be03007a301217f90dcc20333bd9a",
"updTime" : "2024-06-10 17:26:53",
"sortId" : 1487918143,
"name" : "酷我音乐车机版 6.4.2.20.apk",
"fileDownloads" : 109,
"fileUrl" : null,
"fileLikes" : 0,
"fileType" : 1,
"fileId" : 1487918143
}
*/
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
parserDir(id, shareId, promise);
} else {
promise.fail("解析目录ID失败");
}
}).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes);
});
return promise.future();
}
// 映射已知字段fileInfo
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject;
try {
jsonObject = asJson(res);
} catch (Exception e) {
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
return;
}
// System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 其他参数
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2Hex(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
// 映射已知字段fileInfo
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
// 其他参数
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2Hex(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileJson.getString("fileId"))
.setCreateTime(fileJson.getString("createTime"))
.setFileType(fileJson.getString("fileType"))
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
});
promise.complete(result);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileJson.getString("fileId"))
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
});
return promise.future();
promise.complete(result);
}).onFailure(failRes -> {
log.error("解析目录请求失败: {}", failRes.getMessage());
promise.fail(failRes);
});;
}
@Override
public Future<String> parseById() {
// 第二次请求
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
// clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
// .putHeaders(header)
// .setTemplateParam("fidEncode", fidEncode)
// .setTemplateParam("uuid", uuid)
// .setTemplateParam("ts", tsEncode2)
// .setTemplateParam("auth", auth)
// .setTemplateParam("dataKey", shareId)
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))

View File

@@ -12,6 +12,7 @@ import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
@@ -77,17 +78,15 @@ public class IzTool extends PanBase {
// 第一次请求 获取文件信息
// POST https://api.ilanzou.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(r0 -> { // 忽略res
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
client.postAbs(UriTemplate.of(
shareLinkInfo.getSharePassword() == null ?
FIRST_REQUEST_URL : (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword()))
)
client.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
@@ -98,14 +97,21 @@ public class IzTool extends PanBase {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (resJson.getJsonArray("list").size() == 0) {
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
// 如果是目录返回目录ID
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
return;
}
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
if (fileList.getInteger("fileType") == 2) {
promise.complete(fileList.getInteger("folderId").toString());
@@ -143,65 +149,115 @@ public class IzTool extends PanBase {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
parserDir(id, shareId, promise);
} else {
promise.fail("解析目录ID失败");
}
}).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes);
});
return promise.future();
}
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject;
try {
jsonObject = asJson(res);
} catch (Exception e) {
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
return;
}
// System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType(fileJson.getString("fileType"))
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
});
promise.complete(result);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
});
return promise.future();
promise.complete(result);
}).onFailure(failRes -> {
log.error("解析目录请求失败: {}", failRes.getMessage());
promise.fail(failRes);
});
}
@Override

View File

@@ -28,7 +28,7 @@ import java.util.regex.Pattern;
*/
public class LzTool extends PanBase {
public static final String SHARE_URL_PREFIX = "https://wwwa.lanzoux.com";
public static final String SHARE_URL_PREFIX = "https://wwww.lanzoup.com";
public LzTool(ShareLinkInfo shareLinkInfo) {
@@ -222,7 +222,9 @@ public class LzTool extends PanBase {
.setSizeStr(fileJson.getString("size"))
.setSize(sizeNum)
.setPanType(panType)
.setParserUrl(getDomainName() + "/d/" + panType + "/" + id);
.setParserUrl(getDomainName() + "/d/" + panType + "/" + id)
.setPreviewUrl(String.format("%s/v2/view/%s/%s", getDomainName(),
shareLinkInfo.getType(), id));
log.debug("文件信息: {}", fileInfo);
list.add(fileInfo);
});

View File

@@ -31,18 +31,20 @@ public class P118Tool extends PanBase {
Pattern compile = Pattern.compile("href=\"([^\"]+)\"");
Matcher matcher = compile.matcher(res.bodyAsString());
if (matcher.find()) {
System.out.println(matcher.group(1));
complete(matcher.group(1));
//c: 0x63
//o: 0x6F
//m: 0x6D
//1: 0x31
///: 0x2F
char[] chars1 = new char[]{99, 111, 109, 49, 47};
char[] chars2 = new char[]{99, 111, 109, 47};
String group = matcher.group(1).replace(String.valueOf(chars1), String.valueOf(chars2));
System.out.println(group);
complete(group);
} else {
fail();
}
}).onFailure(handleFail(""));
return future();
}
// public static void main(String[] args) {
// String s = new P118Tool(ShareLinkInfo.newBuilder().shareUrl("https://xiguage.118pan.com/b11848261").shareKey(
// "11848261").build()).parseSync();
// System.out.println(s);
// }
}

View File

@@ -1,12 +1,21 @@
package cn.qaiu.parser.impl;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.FileSizeConverter;
import cn.qaiu.util.HeaderUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.pointer.JsonPointer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.uritemplate.UriTemplate;
import java.util.List;
/**
* 微雨云
*/
@@ -14,6 +23,10 @@ public class PvyyTool extends PanBase {
private static final String API_URL_PREFIX1 = "https://www.vyuyun.com/apiv1/share/file/{key}?password={pwd}";
private static final String API_URL_PREFIX2 = "https://www.vyuyun.com/apiv1/share/getShareDownUrl/{key}/{id}?password={pwd}";
byte[] hexArray = {
0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x31, 0x36, 0x2e, 0x32, 0x30, 0x35, 0x2e,
0x39, 0x36, 0x2e, 0x31, 0x39, 0x38, 0x3a, 0x33, 0x30, 0x30, 0x30, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x2f
};
private static final MultiMap header = HeaderUtils.parseHeaders("""
accept-language: zh-CN,zh;q=0.9,en;q=0.8
@@ -32,21 +45,52 @@ public class PvyyTool extends PanBase {
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
""");
private final String api;
public PvyyTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
api = new String(hexArray);
}
@Override
public Future<String> parse() {
//
// 请求downcode
WebClient.create(WebClientVertxInit.get())
.getAbs(api + shareLinkInfo.getShareKey())
.send()
.onSuccess(res -> {
if (res.statusCode() == 200) {
String code = res.bodyAsString();
log.info("vyy url:{}, code:{}", shareLinkInfo.getStandardUrl(), code);
String downApi = API_URL_PREFIX2 + "&downcode=" + code;
getDownUrl(downApi);
} else {
fail("code获取失败");
}
}).onFailure(handleFail("code服务异常"));
return future();
}
private void getDownUrl(String apiUrl) {
client.getAbs(UriTemplate.of(API_URL_PREFIX1))
.setTemplateParam("key", shareLinkInfo.getShareKey())
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
.putHeaders(header)
.send().onSuccess(res -> {
try {
String id = asJson(res).getJsonObject("data").getJsonObject("data").getString("id");
JsonObject resJson = asJson(res);
if (!resJson.containsKey("code") || resJson.getInteger("code") != 0) {
fail("获取文件信息失败: " + resJson.getString("message"));
return;
}
JsonObject fileData = resJson.getJsonObject("data").getJsonObject("data");
if (fileData == null) {
fail("文件数据为空");
return;
}
setFileInfo(fileData);
String id = fileData.getString("id");
client.getAbs(UriTemplate.of(API_URL_PREFIX2))
client.getAbs(UriTemplate.of(apiUrl))
.setTemplateParam("key", shareLinkInfo.getShareKey())
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
.setTemplateParam("id", id)
@@ -61,10 +105,117 @@ public class PvyyTool extends PanBase {
}
});
} catch (Exception ignored) {
fail(asJson(res).encodePrettily());
fail();
}
});
return future();
}
private void setFileInfo(JsonObject fileData) {
JsonObject attributes = fileData.getJsonObject("attributes");
JsonObject user = (JsonObject)(JsonPointer.from("/relationships/user/data").queryJson(fileData));
int downCount = (Integer)(JsonPointer.from("/relationships/shared/data/attributes/down").queryJson(fileData));
String filesize = attributes.getString("filesize");
FileInfo fileInfo = new FileInfo()
.setFileId(fileData.getString("id"))
.setFileName(attributes.getString("basename"))
.setFileType(attributes.getString("mimetype"))
.setPanType(shareLinkInfo.getType())
.setCreateBy(user.getString("email"))
.setDownloadCount(downCount)
.setSize(FileSizeConverter.convertToBytes(filesize))
.setSizeStr(filesize);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
}
private static final String DIR_API = "https://www.vyuyun.com/apiv1/share/folders/809Pt6/bMjnUg?sort=created_at&direction=DESC&password={pwd}";
private static final String SHARE_TYPE_API = "https://www.vyuyun.com/apiv1/share/info/{key}?password={pwd}";
//
// @Override
// public Future<List<FileInfo>> parseFileList() {
// Promise<List<FileInfo>> promise = Promise.promise();
// client.getAbs(UriTemplate.of(SHARE_TYPE_API))
// .setTemplateParam("key", shareLinkInfo.getShareKey())
// .setTemplateParam("pwd", shareLinkInfo.getSharePassword()).send().onSuccess(res -> {
// // "data" -> "attributes"->type
// String type = asJson(res).getJsonObject("data").getJsonObject("attributes").getString("type");
// if ("folder".equals(type)) {
// // 文件夹
// client.getAbs(UriTemplate.of(DIR_API))
// .setTemplateParam("key", shareLinkInfo.getShareKey())
// .setTemplateParam("pwd", shareLinkInfo.getSharePassword())
// .send().onSuccess(res2 -> { try {
//
// try {
// // 新的解析逻辑
// var arr = asJson(res2).getJsonObject("data").getJsonArray("data");
// List<FileInfo> list = arr.stream().map(o -> {
// FileInfo fileInfo = new FileInfo();
// var jo = ((io.vertx.core.json.JsonObject) o).getJsonObject("data");
// String fileType = jo.getString("type");
// fileInfo.setFileId(jo.getString("id"));
// fileInfo.setFileName(jo.getJsonObject("attributes").getString("name"));
// // 文件大小可能为null或字符串
// Object sizeObj = jo.getJsonObject("attributes").getValue("filesize");
// if (sizeObj instanceof Number) {
// fileInfo.setSize(((Number) sizeObj).longValue());
// } else if (sizeObj instanceof String sizeStr) {
// try {
// getSize(fileInfo, sizeStr);
// } catch (Exception e) {
// fileInfo.setSize(0L);
// }
// } else {
// fileInfo.setSize(0L);
// }
// fileInfo.setFileType("folder".equals(fileType) ? "folder" : "file");
// return fileInfo;
// }).toList();
// promise.complete(list);
// } catch (Exception ignored) {
// promise.fail(asJson(res2).encodePrettily());
// }
// }).onFailure(t->{
// promise.fail("获取文件夹内容失败: " + t.getMessage());
// });
// } else if ("file".equals(type)) {
// // 单文件
// FileInfo fileInfo = new FileInfo();
// var jo = asJson(res).getJsonObject("data").getJsonObject("attributes");
// fileInfo.setFileId(asJson(res).getJsonObject("data").getString("id"));
// fileInfo.setFileName(jo.getString("name"));
// Object sizeObj = jo.getValue("filesize");
// if (sizeObj instanceof Number) {
// fileInfo.setSize(((Number) sizeObj).longValue());
// } else if (sizeObj instanceof String sizeStr) {
// try {
// getSize(fileInfo, sizeStr);
// } catch (Exception e) {
// fileInfo.setSize(0L);
// }
// } else {
// fileInfo.setSize(0L);
// }
// fileInfo.setFileType("file");
// promise.complete(List.of(fileInfo));
// } else {
// promise.fail("未知的分享类型");
// }
// });
// return promise.future();
// }
//
// private void getSize(FileInfo fileInfo, String sizeStr) {
// if (sizeStr.endsWith("KB")) {
// fileInfo.setSize(Long.parseLong(sizeStr.replace("KB", "").trim()) * 1024);
// } else if (sizeStr.endsWith("MB")) {
// fileInfo.setSize(Long.parseLong(sizeStr.replace("MB", "").trim()) * 1024 * 1024);
// } else {
// fileInfo.setSize(Long.parseLong(sizeStr));
// }
// }
//
// @Override
// public Future<String> parseById() {
// return super.parseById();
// }
}

View File

@@ -0,0 +1,171 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.HeaderUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* QQ闪传 <br>
* 只能客户端上传 支持Android QQ 9.2.5, MACOS QQ 6.9.78可生成分享链接通过浏览器下载支持超大文件有效期默认7天暂时没找到续期方法。<br>
*/
public class QQscTool extends PanBase {
Logger LOG = LoggerFactory.getLogger(QQscTool.class);
private static final String API_URL = "https://qfile.qq.com/http2rpc/gotrpc/noauth/trpc.qqntv2.richmedia.InnerProxy/BatchDownload";
private static final MultiMap HEADERS = HeaderUtils.parseHeaders("""
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Cookie: uin=9000002; p_uin=9000002
DNT: 1
Origin: https://qfile.qq.com
Referer: https://qfile.qq.com/q/Xolxtv5b4O
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0
accept: application/json
content-type: application/json
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
x-oidb: {"uint32_command":"0x9248", "uint32_service_type":"4"}
""");
public QQscTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
String jsonTemplate = """
{"req_head":{"agent":8},"download_info":[{"batch_id":"%s","scene":{"business_type":4,"app_type":22,"scene_type":5},"index_node":{"file_uuid":"%s"},"url_type":2,"download_scene":0}],"scene_type":103}
""";
client.getAbs(shareLinkInfo.getShareUrl()).send(result -> {
if (result.succeeded()) {
String htmlJs = result.result().bodyAsString();
LOG.debug("获取到的HTML内容: {}", htmlJs);
String fileUUID = getFileUUID(htmlJs);
String fileName = extractFileNameFromTitle(htmlJs);
if (fileName != null) {
LOG.info("提取到的文件名: {}", fileName);
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(fileName);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
} else {
LOG.warn("未能提取到文件名");
}
if (fileUUID != null) {
LOG.info("提取到的文件UUID: {}", fileUUID);
String formatted = jsonTemplate.formatted(fileUUID, fileUUID);
JsonObject entries = new JsonObject(formatted);
client.postAbs(API_URL)
.putHeaders(HEADERS)
.sendJsonObject(entries)
.onSuccess(result2 -> {
if (result2.statusCode() == 200) {
JsonObject body = asJson(result2);
LOG.debug("API响应内容: {}", body.encodePrettily());
// {
// "retcode": 0,
// "cost": 132,
// "message": "",
// "error": {
// "message": "",
// "code": 0
// },
// "data": {
// "download_rsp": [{
// 取 download_rsp
if (!body.containsKey("retcode") || body.getInteger("retcode") != 0) {
promise.fail("API请求失败错误信息: " + body.encodePrettily());
return;
}
JsonArray downloadRsp = body.getJsonObject("data").getJsonArray("download_rsp");
if (downloadRsp != null && !downloadRsp.isEmpty()) {
String url = downloadRsp.getJsonObject(0).getString("url");
if (fileName != null) {
url = url + "&filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8);
}
promise.complete(url);
} else {
promise.fail("API响应中缺少 download_rsp");
}
} else {
promise.fail("API请求失败状态码: " + result2.statusCode());
}
}).onFailure(e -> {
LOG.error("API请求异常", e);
promise.fail(e);
});
} else {
LOG.error("未能提取到文件UUID");
promise.fail("未能提取到文件UUID");
}
} else {
LOG.error("请求失败: {}", result.cause().getMessage());
promise.fail(result.cause());
}
});
return promise.future();
}
String getFileUUID(String htmlJs) {
String keyword = "\"download_limit_status\"";
String marker = "},\"";
int startIndex = htmlJs.indexOf(keyword);
if (startIndex != -1) {
int markerIndex = htmlJs.indexOf(marker, startIndex);
if (markerIndex != -1) {
int quoteStart = markerIndex + marker.length();
int quoteEnd = htmlJs.indexOf("\"", quoteStart);
if (quoteEnd != -1) {
String extracted = htmlJs.substring(quoteStart, quoteEnd);
LOG.debug("提取结果: {}", extracted);
return extracted;
} else {
LOG.error("未找到结束引号: {}", marker);
}
} else {
LOG.error("未找到标记: {} 在关键字: {} 之后", marker, keyword);
}
} else {
LOG.error("未找到关键字: {}", keyword);
}
return null;
}
public static String extractFileNameFromTitle(String content) {
// 匹配<title>和</title>之间的内容
Pattern pattern = Pattern.compile("<title>(.*?)</title>");
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
String fullTitle = matcher.group(1);
// 按 "" 分割,取前半部分
int sepIndex = fullTitle.indexOf("");
if (sepIndex != -1) {
return fullTitle.substring(0, sepIndex);
}
return fullTitle; // 如果没有分隔符,就返回全部
}
return null;
}
}

View File

@@ -1,5 +1,6 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import io.vertx.core.Future;
import java.util.HashMap;
@@ -15,9 +16,15 @@ public class QQwTool extends QQTool {
@Override
public Future<String> parse() {
client.getAbs(shareLinkInfo.getStandardUrl()).send().onSuccess(res -> {
client.getAbs(shareLinkInfo.getShareUrl()).send().onSuccess(res -> {
String html = res.bodyAsString();
String url = extractVariables(html).get("url");
Map<String, String> stringStringMap = extractVariables(html);
String url = stringStringMap.get("url");
String fn = stringStringMap.get("filename");
String size = stringStringMap.get("filesize");
String createBy = stringStringMap.get("nick");
FileInfo fileInfo = new FileInfo().setFileName(fn).setSize(Long.parseLong(size)).setCreateBy(createBy);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
if (url != null) {
String url302 = url.replace("\\x26", "&");
promise.complete(url302);
@@ -44,16 +51,17 @@ public class QQwTool extends QQTool {
private Map<String, String> extractVariables(String jsCode) {
Map<String, String> variables = new HashMap<>();
// 正则表达式匹配 var 变量定义
String regex = "var\\s+(\\w+)\\s*=\\s*([\"']?)([^\"';\\s]+)\\2\n";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(jsCode);
String regex = "\\s+var\\s+(\\w+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^;\\r\\n]*))";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(jsCode);
while (matcher.find()) {
String variableName = matcher.group(1); // 变量名
String variableValue = matcher.group(3); // 变量值
variables.put(variableName, variableValue);
while (m.find()) {
String name = m.group(1);
String value = m.group(2) != null ? m.group(2)
: m.group(3) != null ? m.group(3)
: m.group(4);
variables.put(name, value);
}
return variables;

View File

@@ -1,19 +1,26 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter;
import cn.qaiu.util.JsExecUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.pointer.JsonPointer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,9 +37,10 @@ public class YeTool extends PanBase {
private static final String GET_FILE_INFO_URL = "https://www.123pan.com/a/api/share/get?limit=100&next=1&orderBy" +
"=file_name&orderDirection=asc" +
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId=0&Page=1&event=homeListFile&operateType=1";
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId={ParentFileId}&Page=1&event=homeListFile&operateType=1";
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/a/api/share/download/info?{authK}={authV}";
private static final String BATCH_DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/batch_download_share_info?{authK}={authV}";
private final MultiMap header = MultiMap.caseInsensitiveMultiMap();
public YeTool(ShareLinkInfo shareLinkInfo) {
@@ -41,8 +49,8 @@ public class YeTool extends PanBase {
header.set("App-Version", "3");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
header.set("DNT", "1");
header.set("Host", "www.123pan.com");
//header.set("DNT", "1");
//header.set("Host", "www.123pan.com");
header.set("LoginUuid", gen36String());
header.set("Pragma", "no-cache");
header.set("Referer", shareLinkInfo.getStandardUrl());
@@ -94,6 +102,7 @@ public class YeTool extends PanBase {
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", pwd)
.setTemplateParam("ParentFileId", "0")
// .setTemplateParam("authKey", AESUtils.getAuthKey("/a/api/share/get"))
.putHeader("Platform", "web")
.putHeader("App-Version", "3")
@@ -103,9 +112,23 @@ public class YeTool extends PanBase {
fail("{} 状态码异常 {}", dataKey, infoJson);
return;
}
JsonObject getFileInfoJson =
infoJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
getFileInfoJson.put("ShareKey", shareKey);
// 判断是否为文件夹: data->InfoList->0->Type: 1为文件夹, 0为文件
try {
int type = (Integer)JsonPointer.from("/data/InfoList/0/Type").queryJson(infoJson);
if (type == 1) {
getZipDownUrl(client, getFileInfoJson);
return;
}
} catch (Exception exception) {
fail("该分享[{}]解析异常: {}", dataKey, exception.getMessage());
return;
}
getDownUrl(client, getFileInfoJson);
}).onFailure(this.handleFail(GET_FILE_INFO_URL));
} else {
@@ -116,6 +139,11 @@ public class YeTool extends PanBase {
JsonObject reqBodyJson = resListJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
reqBodyJson.put("ShareKey", shareKey);
if (reqBodyJson.getInteger("Type") == 1) {
// 文件夹
getZipDownUrl(client, reqBodyJson);
return;
}
getDownUrl(client, reqBodyJson);
}).onFailure(this.handleFail(FIRST_REQUEST_URL));
@@ -134,6 +162,21 @@ public class YeTool extends PanBase {
jsonObject.put("Etag", reqBodyJson.getString("Etag"));
// 调用JS文件获取签名
down(client, jsonObject, DOWNLOAD_API_URL);
}
private void getZipDownUrl(WebClient client, JsonObject reqBodyJson) {
log.info(reqBodyJson.encodePrettily());
JsonObject jsonObject = new JsonObject();
// {"ShareKey":"LH3rTd-1ENed","fileIdList":[{"fileId":17525952}]}
jsonObject.put("ShareKey", reqBodyJson.getString("ShareKey"));
jsonObject.put("fileIdList", new JsonArray().add(JsonObject.of("fileId", reqBodyJson.getInteger("FileId"))));
// 调用JS文件获取签名
down(client, jsonObject, BATCH_DOWNLOAD_API_URL);
}
private void down(WebClient client, JsonObject jsonObject, String api) {
ScriptObjectMirror getSign;
try {
getSign = JsExecUtils.executeJs("getSign", "/a/api/share/download/info");
@@ -143,7 +186,7 @@ public class YeTool extends PanBase {
}
log.info("ye getSign: {}={}", getSign.get("0").toString(), getSign.get("1").toString());
client.postAbs(UriTemplate.of(DOWNLOAD_API_URL))
client.postAbs(UriTemplate.of(api))
.setTemplateParam("authK", getSign.get("0").toString())
.setTemplateParam("authV", getSign.get("1").toString())
.putHeader("Platform", "web")
@@ -160,7 +203,8 @@ public class YeTool extends PanBase {
fail("Ye: downURLJson格式异常->" + downURLJson);
return;
}
String downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
String downURL = downURLJson.getJsonObject("data")
.getString(api.contains("batch_download_share_info")? "DownloadUrl" : "DownloadURL");
try {
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
String params = urlParams.get("params");
@@ -189,4 +233,121 @@ public class YeTool extends PanBase {
}
}).onFailure(this.handleFail(DOWNLOAD_API_URL));
}
// dir parser
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String shareKey = shareLinkInfo.getShareKey(); // 分享链接的唯一标识
String pwd = shareLinkInfo.getSharePassword(); // 分享密码
String parentFileId = "0"; // 根目录的文件ID
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (StringUtils.isNotBlank(dirId)) {
parentFileId = dirId;
}
// 构造文件列表接口的URL
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", pwd)
.setTemplateParam("ParentFileId", parentFileId)
.putHeaders(header)
.send().onSuccess(res -> {
JsonObject response = asJson(res);
if (response.getInteger("code") != 0) {
promise.fail("API错误: " + response.getString("message"));
return;
}
JsonArray infoList = response.getJsonObject("data").getJsonArray("InfoList");
List<FileInfo> result = new ArrayList<>();
// 遍历返回的文件和目录信息
for (int i = 0; i < infoList.size(); i++) {
JsonObject item = infoList.getJsonObject(i);
FileInfo fileInfo = new FileInfo();
// "FileId": 16603582,
// "FileName": "pdf",
// "Type": 1,
// "Size": 0,
// "ContentType": "0",
// "S3KeyFlag": "",
// "CreateAt": "2025-07-09T06:56:20+08:00",
// "UpdateAt": "2025-07-09T06:56:20+08:00",
// "Etag": "",
// "DownloadUrl": "",
// "Status": 0,
// "ParentFileId": 16603579,
// "Category": 0,
// "PunishFlag": 0,
// "StorageNode": "m0",
// "PreviewType": 0
// =>
// {
// "ShareKey":"iaKtVv-FTaCd",
// "FileID":16604189,
// "S3keyFlag":"1815268665-0",
// "Size":425929,
// "Etag":"70049de67075ab2b269c62d690424601",
// "OrderId":""}
JsonObject postData = JsonObject.of()
.put("ShareKey", shareKey)
.put("FileID", item.getInteger("FileId"))
.put("S3keyFlag", item.getString("S3KeyFlag"))
.put("Size", item.getLong("Size"))
.put("Etag", item.getString("Etag"));
byte[] encode = Base64.getEncoder().encode(postData.encode().getBytes());
String param = new String(encode);
if (item.getInteger("Type") == 0) { // 文件
fileInfo.setFileName(item.getString("FileName"))
.setFileId(item.getString("FileId"))
.setFileType("file")
.setSize(item.getLong("Size"))
.setCreateTime(item.getString("CreateAt"))
.setUpdateTime(item.getString("UpdateAt"))
.setSizeStr(FileSizeConverter.convertToReadableSize(item.getLong("Size")))
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
} else if (item.getInteger("Type") == 1) { // 目录
fileInfo.setFileName(item.getString("FileName"))
.setFileId(item.getString("FileId"))
.setCreateTime(item.getString("CreateAt"))
.setUpdateTime(item.getString("UpdateAt"))
.setSize(0L)
.setFileType("folder")
.setParserUrl(
String.format("%s/v2/getFileList?url=%s&dirId=%s&pwd=%s",
getDomainName(),
shareLinkInfo.getShareUrl(),
item.getString("FileId"),
pwd)
);
result.add(fileInfo);
}
}
promise.complete(result);
}).onFailure(promise::fail);
return promise.future();
}
@Override
public Future<String> parseById() {
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
// 调用下载接口获取直链
down(client, paramJson, DOWNLOAD_API_URL);
return promise.future();
}
}

View File

@@ -7,8 +7,17 @@ public class FileSizeConverter {
throw new IllegalArgumentException("Invalid file size string");
}
sizeStr = sizeStr.trim().toUpperCase();
char unit = sizeStr.charAt(sizeStr.length() - 1);
sizeStr = sizeStr.replace(",","").trim().toUpperCase();
// 判断是2位单位还是1位单位
// 判断单位是否为2位
int unitIndex = sizeStr.length() - 1;
char unit = sizeStr.charAt(unitIndex);
if (Character.isLetter(sizeStr.charAt(unitIndex - 1))) {
unit = sizeStr.charAt(unitIndex - 1);
sizeStr = sizeStr.substring(0, unitIndex - 1);
} else {
sizeStr = sizeStr.substring(0, unitIndex);
}
double size = Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 1));
return switch (unit) {

View File

@@ -27,9 +27,9 @@
<vertx.version>4.5.6</vertx.version>
<org.reflections.version>0.10.2</org.reflections.version>
<lombok.version>1.18.30</lombok.version>
<lombok.version>1.18.38</lombok.version>
<slf4j.version>2.0.5</slf4j.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>
<jackson.version>2.14.2</jackson.version>
<logback.version>1.5.8</logback.version>

View File

@@ -10,7 +10,7 @@
## 关于如何将前端项目和java一块打包:
1. 先打包前端模块
2. ~~打包后请将当前目录下的nfd-front目录放置在项目下webroot目录, 然后使用maven打包java模块即可~~ `npm run build` 会直接打包到后端代理目录下, 无需复制
2. 运行`npm run build`
3. 项目部署后演示页面的代理端口是6401默认使用http, 如需https可以加nginx代理, 也可以使用本项目自带的代理服务和配置证书路径
## nginx配置

View File

@@ -1,5 +1,8 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
'@vue/babel-plugin-transform-vue-jsx'
]
}

View File

@@ -4,29 +4,34 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^11.2.0",
"axios": "^1.7.4",
"axios": "1.11.0",
"clipboard": "^2.0.11",
"core-js": "^3.8.3",
"element-plus": "^2.8.7",
"qrcode": "^1.5.4",
"splitpanes": "^4.0.4",
"vue": "^3.5.12",
"vue-clipboard3": "^2.0.0",
"vue3-json-viewer": "^2.2.2"
"vue-router": "^4.5.1",
"vue3-json-viewer": "2.2.2"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@babel/plugin-transform-class-properties": "^7.26.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.4.0",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"compression-webpack-plugin": "^11.1.0",
"eslint": "^9.14.0",
"eslint": "^9.0.0",
"eslint-plugin-vue": "^9.30.0",
"filemanager-webpack-plugin": "8.0.0"
},
@@ -48,5 +53,12 @@
"> 1%",
"last 2 versions",
"not dead"
]
],
"engines": {
"node": ">=16.0.0 <=22.0.0",
"npm": ">=8.0.0"
},
"overrides": {
"eslint": "^9.0.0"
}
}

View File

@@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2024 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

9
web-front/public/css/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -9,6 +9,8 @@
content="Netdisk fast download,网盘直链解析工具">
<meta name="description"
content="Netdisk fast download 网盘直链解析工具">
<!-- Font Awesome 图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.page-loading-wrap {
padding: 120px;
@@ -151,6 +153,13 @@
}
}
</style>
<script>
const saved = localStorage.getItem('isDarkMode') === 'true'
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (saved || (!saved && systemDark)) {
document.body.classList.add('dark-theme')
}
</script>
</head>
<body>
<div id="app">

693
web-front/public/list.html Normal file
View File

@@ -0,0 +1,693 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网盘目录管理系统</title>
<!-- 本地引用Font Awesome -->
<link rel="stylesheet" href="./css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
}
h1 {
color: #2c3e50;
font-size: 2.5rem;
margin-bottom: 10px;
position: relative;
display: inline-block;
}
h1:after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 4px;
background: linear-gradient(90deg, #3498db, #2c3e50);
border-radius: 2px;
}
.subtitle {
color: #7f8c8d;
font-size: 1.1rem;
margin-top: 5px;
}
.dashboard {
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.breadcrumb {
background: #f8f9fa;
padding: 16px 24px;
border-bottom: 1px solid #eaeaea;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.breadcrumb-item {
display: flex;
align-items: center;
font-size: 0.95rem;
color: #7f8c8d;
cursor: pointer;
transition: color 0.2s;
}
.breadcrumb-item:hover {
color: #3498db;
}
.breadcrumb-item i {
margin: 0 8px;
font-size: 0.8rem;
color: #bdc3c7;
}
.content {
padding: 24px;
min-height: 500px;
}
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 20px;
}
.item {
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
cursor: pointer;
text-align: center;
padding: 20px 10px;
border: 2px solid transparent;
}
.item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
border-color: #3498db;
}
.item-icon {
font-size: 3.5rem;
margin-bottom: 15px;
transition: transform 0.3s;
}
.item:hover .item-icon {
transform: scale(1.1);
}
.folder .item-icon {
color: #3498db;
}
.image .item-icon {
color: #e74c3c;
}
.document .item-icon {
color: #f39c12;
}
.archive .item-icon {
color: #9b59b6;
}
.audio .item-icon {
color: #1abc9c;
}
.video .item-icon {
color: #d35400;
}
.code .item-icon {
color: #27ae60;
}
.item-name {
font-weight: 500;
font-size: 0.95rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
}
.item-meta {
font-size: 0.8rem;
color: #95a5a6;
margin-top: 8px;
}
.empty-state {
text-align: center;
padding: 50px 20px;
color: #7f8c8d;
grid-column: 1 / -1;
}
.empty-state i {
font-size: 5rem;
margin-bottom: 20px;
color: #bdc3c7;
}
.empty-state h3 {
font-size: 1.5rem;
margin-bottom: 10px;
color: #2c3e50;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
grid-column: 1 / -1;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(52, 152, 219, 0.2);
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.action-bar {
display: flex;
justify-content: space-between;
padding: 15px 24px;
background: #f8f9fa;
border-top: 1px solid #eaeaea;
}
.btn {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: background 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background: #2980b9;
}
.btn i {
font-size: 0.9rem;
}
.stats {
display: flex;
align-items: center;
gap: 15px;
color: #7f8c8d;
font-size: 0.9rem;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
@media (max-width: 768px) {
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 15px;
}
.item {
padding: 15px 8px;
}
.item-icon {
font-size: 3rem;
}
}
@media (max-width: 480px) {
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 10px;
}
h1 {
font-size: 2rem;
}
.action-bar {
flex-direction: column;
gap: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-cloud"></i> 网盘目录管理系统</h1>
<p class="subtitle">管理您的文件与文件夹,操作简单直观</p>
</header>
<div class="dashboard">
<div class="breadcrumb" id="breadcrumb">
<!-- 面包屑导航会通过JS动态生成 -->
</div>
<div class="content">
<div class="grid-view" id="file-grid">
<!-- 文件列表会通过JS动态生成 -->
</div>
</div>
<div class="action-bar">
<button class="btn" id="back-btn">
<i class="fas fa-arrow-left"></i> 返回上一级
</button>
<div class="stats">
<div class="stat-item">
<i class="fas fa-folder"></i> <span id="folder-count">0</span> 个文件夹
</div>
<div class="stat-item">
<i class="fas fa-file"></i> <span id="file-count">0</span> 个文件
</div>
</div>
</div>
</div>
</div>
<script>
// 文件类型映射
const fileTypeIcons = {
// 图片
'jpg': { icon: 'fa-file-image', type: 'image' },
'jpeg': { icon: 'fa-file-image', type: 'image' },
'png': { icon: 'fa-file-image', type: 'image' },
'gif': { icon: 'fa-file-image', type: 'image' },
'bmp': { icon: 'fa-file-image', type: 'image' },
'svg': { icon: 'fa-file-image', type: 'image' },
'webp': { icon: 'fa-file-image', type: 'image' },
// 文档
'pdf': { icon: 'fa-file-pdf', type: 'document' },
'doc': { icon: 'fa-file-word', type: 'document' },
'docx': { icon: 'fa-file-word', type: 'document' },
'xls': { icon: 'fa-file-excel', type: 'document' },
'xlsx': { icon: 'fa-file-excel', type: 'document' },
'ppt': { icon: 'fa-file-powerpoint', type: 'document' },
'pptx': { icon: 'fa-file-powerpoint', type: 'document' },
'txt': { icon: 'fa-file-alt', type: 'document' },
'rtf': { icon: 'fa-file-alt', type: 'document' },
// 压缩文件
'zip': { icon: 'fa-file-archive', type: 'archive' },
'rar': { icon: 'fa-file-archive', type: 'archive' },
'7z': { icon: 'fa-file-archive', type: 'archive' },
'tar': { icon: 'fa-file-archive', type: 'archive' },
'gz': { icon: 'fa-file-archive', type: 'archive' },
// 音频
'mp3': { icon: 'fa-file-audio', type: 'audio' },
'wav': { icon: 'fa-file-audio', type: 'audio' },
'ogg': { icon: 'fa-file-audio', type: 'audio' },
'flac': { icon: 'fa-file-audio', type: 'audio' },
// 视频
'mp4': { icon: 'fa-file-video', type: 'video' },
'avi': { icon: 'fa-file-video', type: 'video' },
'mov': { icon: 'fa-file-video', type: 'video' },
'wmv': { icon: 'fa-file-video', type: 'video' },
'mkv': { icon: 'fa-file-video', type: 'video' },
'flv': { icon: 'fa-file-video', type: 'video' },
// 代码
'html': { icon: 'fa-file-code', type: 'code' },
'htm': { icon: 'fa-file-code', type: 'code' },
'css': { icon: 'fa-file-code', type: 'code' },
'js': { icon: 'fa-file-code', type: 'code' },
'json': { icon: 'fa-file-code', type: 'code' },
'php': { icon: 'fa-file-code', type: 'code' },
'py': { icon: 'fa-file-code', type: 'code' },
'java': { icon: 'fa-file-code', type: 'code' },
'c': { icon: 'fa-file-code', type: 'code' },
'cpp': { icon: 'fa-file-code', type: 'code' },
'h': { icon: 'fa-file-code', type: 'code' },
'sh': { icon: 'fa-file-code', type: 'code' },
'bat': { icon: 'fa-file-code', type: 'code' },
'md': { icon: 'fa-file-code', type: 'code' },
// 默认
'default': { icon: 'fa-file', type: 'document' }
};
const obj = new URL(window.location.href);
// 获取 URL 参数
const params = obj.searchParams;
const shareUrl = params.get('url');
const pwd = params.get('pwd');
// 动态拼接并编码参数
const apiUrl = `${window.location.origin}/v2/getFileList?url=${encodeURIComponent(shareUrl)}&pwd=${encodeURIComponent(pwd)}`;
// 当前目录状态
let currentDir = {
// url: 'http://192.168.101.227:6401/v2/getFileList?url=https://share.feijipan.com/s/3pMsofZd&pwd=qaiu',
// 动态获取url encode 参数
url: apiUrl,
name: '全部文件'
};
const pathStack = [currentDir];
// DOM 元素
const breadcrumbEl = document.getElementById('breadcrumb');
const fileGridEl = document.getElementById('file-grid');
const backBtn = document.getElementById('back-btn');
const folderCountEl = document.getElementById('folder-count');
const fileCountEl = document.getElementById('file-count');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
renderBreadcrumb();
fetchFileList(currentDir.url);
// 返回上一级按钮事件
backBtn.addEventListener('click', goBack);
});
// 渲染面包屑导航
function renderBreadcrumb() {
breadcrumbEl.innerHTML = '';
pathStack.forEach((item, index) => {
const itemEl = document.createElement('div');
itemEl.className = 'breadcrumb-item';
itemEl.textContent = item.name;
if (index < pathStack.length - 1) {
itemEl.addEventListener('click', () => {
// 点击面包屑项返回对应目录
goToDirectory(index);
});
} else {
itemEl.style.cursor = 'default';
itemEl.style.fontWeight = '600';
itemEl.style.color = '#2c3e50';
}
breadcrumbEl.appendChild(itemEl);
// 如果不是最后一个,添加分隔符
if (index < pathStack.length - 1) {
const separator = document.createElement('i');
separator.className = 'fas fa-chevron-right';
breadcrumbEl.appendChild(separator);
}
});
}
// 获取文件列表
async function fetchFileList(url) {
try {
// 显示加载状态
fileGridEl.innerHTML = `
<div class="loading">
<div class="spinner"></div>
</div>
`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.code === 200 && data.success) {
renderFileList(data.data);
} else {
throw new Error(data.msg || '获取文件列表失败');
}
} catch (error) {
console.error('获取文件列表失败:', error);
fileGridEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-circle"></i>
<h3>加载失败</h3>
<p>${error.message}</p>
</div>
`;
}
}
// 渲染文件列表
function renderFileList(files) {
fileGridEl.innerHTML = '';
if (!files || files.length === 0) {
fileGridEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-folder-open"></i>
<h3>此文件夹为空</h3>
<p>暂无文件或文件夹</p>
</div>
`;
folderCountEl.textContent = '0';
fileCountEl.textContent = '0';
return;
}
let folderCount = 0;
let fileCount = 0;
files.forEach(file => {
const item = document.createElement('div');
if (file.fileType === 'folder') {
// 文件夹
item.className = 'item folder';
item.innerHTML = `
<div class="item-icon">
<i class="fas fa-folder"></i>
</div>
<div class="item-name">${file.fileName || '未命名文件夹'}</div>
<div class="item-meta">
${file.sizeStr || '0B'} · ${formatDate(file.createTime)}
</div>
`;
folderCount++;
// 添加点击事件
item.addEventListener('click', () => {
enterFolder(file);
});
} else {
// 文件
const fileExt = getFileExtension(file.fileName);
const fileTypeInfo = fileTypeIcons[fileExt.toLowerCase()] || fileTypeIcons['default'];
item.className = `item ${fileTypeInfo.type}`;
item.innerHTML = `
<div class="item-icon">
<i class="fas ${fileTypeInfo.icon}"></i>
</div>
<div class="item-name">${file.fileName}</div>
<div class="item-meta">
${file.sizeStr || '0B'} · ${formatDate(file.createTime)}
</div>
`;
fileCount++;
// 添加点击事件
item.addEventListener('click', () => {
handleFileClick(file);
});
}
fileGridEl.appendChild(item);
});
// 更新统计信息
folderCountEl.textContent = folderCount;
fileCountEl.textContent = fileCount;
}
// 获取文件扩展名
function getFileExtension(filename) {
if (!filename) return '';
return filename.split('.').pop();
}
// 进入文件夹
function enterFolder(folder) {
if (!folder.parserUrl) {
alert('无法进入该文件夹,缺少访问链接');
return;
}
const newDir = {
url: folder.parserUrl,
name: folder.fileName || '未命名文件夹'
};
pathStack.push(newDir);
currentDir = newDir;
fetchFileList(currentDir.url);
renderBreadcrumb();
}
// 下载文件
function handleFileClick(file) {
if (!file.parserUrl) {
alert('无法操作该文件,缺少必要链接');
return;
}
// 更友好的选择对话框
const modal = document.createElement('div');
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
modal.style.display = 'flex';
modal.style.justifyContent = 'center';
modal.style.alignItems = 'center';
modal.style.zIndex = '1000';
const dialog = document.createElement('div');
dialog.style.backgroundColor = 'white';
dialog.style.padding = '20px';
dialog.style.borderRadius = '8px';
dialog.style.width = '300px';
dialog.style.textAlign = 'center';
dialog.innerHTML = `
<p style="margin-bottom: 20px;">${file.fileName || '未命名文件'}</p>
<div style="display: flex; justify-content: center; gap: 15px;">
<button id="preview-btn" style="padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">
预览文件
</button>
<button id="download-btn" style="padding: 8px 15px; background: #2ecc71; color: white; border: none; border-radius: 4px; cursor: pointer;">
下载文件
</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// 预览按钮事件
dialog.querySelector('#preview-btn').addEventListener('click', () => {
document.body.removeChild(modal);
const previewUrl = file.previewUrl || file.parserUrl;
window.open(previewUrl, '_blank');
});
// 下载按钮事件
dialog.querySelector('#download-btn').addEventListener('click', () => {
document.body.removeChild(modal);
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = file.parserUrl;
document.body.appendChild(iframe);
setTimeout(() => {
document.body.removeChild(iframe);
}, 3000);
});
// 点击蒙层关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
});
}
// 返回上一级
function goBack() {
if (pathStack.length > 1) {
pathStack.pop();
currentDir = pathStack[pathStack.length - 1];
fetchFileList(currentDir.url);
renderBreadcrumb();
}
}
// 跳转到指定目录
function goToDirectory(index) {
pathStack.splice(index + 1);
currentDir = pathStack[pathStack.length - 1];
fetchFileList(currentDir.url);
renderBreadcrumb();
}
// 格式化日期
function formatDate(dateString) {
if (!dateString) return '未知日期';
try {
const date = new Date(dateString);
return isNaN(date.getTime())
? '未知日期'
: `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
} catch {
return '未知日期';
}
}
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,367 +1,21 @@
<template>
<div id="app">
<el-row :gutter="20">
<el-card class="box-card">
<div style="text-align: right"><DarkMode/></div>
<div class="demo-basic--circle">
<div class="block" style="text-align: center;">
<img :height="150" src="../public/images/lanzou111.png" alt="lz"></img>
</div>
</div>
<h3 style="text-align: center;">NFD网盘直链解析0.1.8_bate32</h3>
<div class="typo">
<p style="text-align: center;">
<span>
<el-link href="https://github.com/qaiu/netdisk-fast-download" target="_blank" rel="nofollow">
<u>GitHub</u></el-link>
</span>
<span style="margin-left: 30px">
<el-link href="https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng" target="_blank"
rel="nofollow"><u>宝塔部署安装教程</u>
</el-link>
</span>
<span style="margin-left: 30px">
<el-link href="https://blog.qaiu.top" target="_blank"
rel="nofollow"><u>QAIU博客</u>
</el-link>
</span></p>
<p><strong>目前支持 </strong>蓝奏云/蓝奏云优享/小飞机盘/123云盘/奶牛快传/移动云云空间/亿方云/文叔叔/QQ邮箱文件中转站</p>
<p>已加入缓存机制, 如果遇到解析出的下载链接失效的情况请及时到<a href="https://github.com/qaiu/netdisk-fast-download/issues">
<u><strong>项目GitHub反馈</strong></u></a></p>
<p>节点1: 回源请求数:{{ node1Info.parserTotal }}, 缓存请求数:{{ node1Info.cacheTotal }}, 总数:{{ node1Info.total }}</p>
<!-- <p>节点2: 成功:{{ node2Info.success }},失败:{{ node2Info.fail }},总数:{{ node2Info.total }}</p>-->
</div>
<hr>
<div class="main" v-loading="isLoading">
<div class="grid-content">
<!-- 开关按钮控制是否自动读取剪切板 -->
<el-switch
v-model="autoReadClipboard"
active-text="自动识别剪切板"
></el-switch>
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url">
<template #prepend>分享链接</template>
<template #append v-if="!autoReadClipboard">
<el-button @click="() => getPaste(1)">读取剪切板</el-button>
</template>
</el-input>
<el-input placeholder="请输入密码" v-model="password" id="url">
<template #prepend>分享密码</template>
</el-input>
<el-input v-show="getLink2" :value="getLink2" id="url">
<template #prepend>智能直链</template>
<template #append>
<el-button v-clipboard:copy="getLink2"
v-clipboard:success="onCopy"
v-clipboard:error="onError">
<el-icon><CopyDocument/></el-icon>
</el-button>
</template>
</el-input>
<p style="text-align: center">
<el-button style="margin-left: 40px;margin-bottom: 10px" @click="onSubmit">解析测试</el-button>
<el-button style="margin-left: 20px;margin-bottom: 10px" @click="genMd">生成Markdown链接</el-button>
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
<el-button style="margin-left: 20px" @click="getTj">链接信息统计</el-button>
</p>
</div>
<div v-if="respData.code" style="margin-top: 10px">
<strong>解析结果: </strong>
<json-viewer
:value="respData"
:expand-depth=5
copyable
boxed
sort
/>
<a :href="downUrl" v-show="downUrl">点击下载</a>
</div>
<div v-if="mdText" style="text-align: center">
<el-input :value="mdText" readonly>
<template #append>
<el-button v-clipboard:copy="mdText"
v-clipboard:success="onCopy"
v-clipboard:error="onError">
<el-icon><CopyDocument/></el-icon>
</el-button>
</template>
</el-input>
</div>
<div style="text-align: center" v-show="showQrc">
<canvas ref="qrcodeCanvas"></canvas>
<div style="text-align: center"><el-link target="_blank" :href="codeUrl">{{ codeUrl }}</el-link></div>
</div>
<div v-if="tjData.shareLinkInfo">
<el-descriptions class="margin-top" title="分享详情" :column="1" border>
<template slot="extra">
<el-button type="primary" size="small">操作</el-button>
</template>
<el-descriptions-item label="网盘名称">{{ tjData.shareLinkInfo.panName }}</el-descriptions-item>
<el-descriptions-item label="网盘标识">{{ tjData.shareLinkInfo.type }}</el-descriptions-item>
<el-descriptions-item label="分享Key">{{ tjData.shareLinkInfo.shareKey }}</el-descriptions-item>
<el-descriptions-item label="分享链接"> <el-link target="_blank" :href="tjData.shareLinkInfo.shareUrl">{{ tjData.shareLinkInfo.shareUrl }}</el-link></el-descriptions-item>
<el-descriptions-item label="jsonApi链接"> <el-link target="_blank" :href="tjData.apiLink">{{ tjData.apiLink }}</el-link></el-descriptions-item>
<el-descriptions-item label="302下载链接"> <el-link target="_blank" :href="tjData.downLink">{{ tjData.downLink }}</el-link></el-descriptions-item>
<el-descriptions-item label="解析次数">{{ tjData.parserTotal }}</el-descriptions-item>
<el-descriptions-item label="缓存命中次数">{{ tjData.cacheHitTotal }}</el-descriptions-item>
<el-descriptions-item label="总请求次数">{{ tjData.sumTotal }}</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-card>
</el-row>
<router-view></router-view>
</div>
</template>
<script>
import axios from 'axios'
import QRCode from 'qrcode'
import DarkMode from '@/components/DarkMode'
import parserUrl from './parserUrl1'
export default {
name: 'App',
components: {DarkMode},
data() {
return {
// baseAPI: `${location.protocol}//${location.hostname}:6400`,
baseAPI: `${location.protocol}//${location.host}`,
autoReadClipboard: true, // 开关状态,默认为自动读取
current: {}, // 当前分享
showQrc: false,
codeUrl: '',
mdText: '',
link: "",
password: "",
isLoading: false,
downUrl: null,
select: "lz",
respData: {},
tjData: {},
panList: [
{
name: "蓝奏云",
value: 'lz'
},
{
name: "奶牛快传",
value: 'cow'
},
{
name: "移动云空间",
value: 'ec'
},
{
name: "UC网盘",
value: 'uc',
disabled: true
},
{
name: "小飞机网盘",
value: 'fj'
},
{
name: "360亿方云",
value: 'fc'
},
{
name: "123云盘",
value: 'ye'
},
],
getLink: null,
getLink2: '',
getLinkInfo: null,
node1Info: {},
node2Info: {},
}
},
methods: {
// toggleDark() {
// toggleDark()
// },
check() {
this.mdText = ''
this.showQrc = false
this.respData = {}
this.tjData = {}
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) {
this.$message.error("请输入有效链接!")
throw new Error('请输入有效链接')
}
},
onSubmit() {
this.check()
this.isLoading = true
this.downUrl = ''
this.respData = {}
this.getLink2 = `${this.baseAPI}/parser?url=${this.link}`
// this.getLink = `${location.protocol}//${location.host}/api/json/parser?url=${this.link}`
// this.getLink = `${location.protocol}//${location.host}/json/parser`
if (this.password) {
this.getLink2 += `&pwd=${this.password}`
}
axios.get(this.getLink, {params: {url: this.link, pwd: this.password}}).then(
response => {
this.isLoading = false
this.respData = response.data
if (response.data.code === 200) {
this.$message({
message: response.data.msg,
type: 'success'
})
this.downUrl = response.data.data.directLink
} else {
this.$message.error(response.data.msg)
}
this.getInfo()
},
error => {
this.isLoading = false
this.$message.error(error.message)
}
)
},
onCopy() {
this.$message.success('复制成功')
},
onError() {
this.$message.error('复制失败')
},
getInfo() {
// 初始化统计信息
axios.get('/v2/statisticsInfo').then(
response => {
if (response.data.success) {
this.node1Info = response.data.data
}
})
// axios.get('/n2/statisticsInfo').then(
// response => {
// if (response.data.success) {
// this.node2Info = response.data.data
// }
// })
},
genMd() {
this.check()
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message({
message: response.data.msg,
type: 'success'
})
this.mdText = this.buildMd('快速下载地址',response.data.data.downLink)
} else {
this.$message.error(response.data.msg)
}
});
},
buildMd(title, url) {
return `[${title}](${url})`
},
generateQRCode() {
this.check()
const options = { // 设置二维码的参数,例如大小、边距等
width: 150,
height: 150,
margin: 2
};
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message({
message: response.data.msg,
type: 'success'
})
this.codeUrl = response.data.data.downLink
QRCode.toCanvas(this.$refs.qrcodeCanvas, this.codeUrl, options, error => {
if (error) console.error(error);
});
this.showQrc = true
} else {
this.$message.error(response.data.msg)
}
});
},
getTj() {
this.check()
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message({
message: response.data.msg,
type: 'success'
})
this.tjData = response.data.data
} else {
this.$message.error(response.data.msg)
}
});
},
async getPaste(v) {
const text = await navigator.clipboard.readText();
console.log('获取到的文本内容是:', text);
let linkInfo = parserUrl.parseLink(text);
let pwd = parserUrl.parsePwd(text) || '';
if (linkInfo.link) {
if(linkInfo.link !== this.link || pwd !== this.password ) {
this.password = pwd;
this.link = linkInfo.link;
this.getLink2 = `${this.baseAPI}/parser?url=${this.link}`
if (this.link) this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`);
} else {
v || this.$message.warning(`[${linkInfo.name}]分享信息无变化`)
}
} else {
this.$message.warning("未能提取到分享链接, 该分享可能尚未支持, 你可以复制任意网盘/音乐App的分享到该页面, 系统智能识别")
}
},
},
mounted() {
this.getLinkInfo = `${this.baseAPI}/v2/linkInfo`
this.getLink = `${this.baseAPI}/json/parser`
let item = window.localStorage.getItem("autoReadClipboard");
if (item) {
this.autoReadClipboard = (item === 'true');
}
this.getInfo()
// 页面首次加载时,根据开关状态判断是否读取剪切板
if (this.autoReadClipboard) {
this.getPaste()
}
// 当文档获得焦点时触发
window.addEventListener('focus', () => {
if (this.autoReadClipboard) {
this.getPaste()
}
});
},
watch: {
autoReadClipboard(val) {
window.localStorage.setItem("autoReadClipboard", val)
}
}
name: 'App'
}
</script>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
@@ -371,62 +25,12 @@ export default {
padding: 1em;
max-width: 900px;
}
body:before {
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: .3;
z-index: -1;
position: fixed;
nav ul {
list-style: none;
padding: 0;
}
.grid-content {
margin-top: 1em;
border-radius: 4px;
min-height: 50px;
}
.el-select .el-input {
width: 130px;
}
.box-card {
margin-top: 4em !important;
margin-bottom: 4em !important;
opacity: .8;
}
@media screen and (max-width: 700px) {
.box-card {
margin-top: 1em !important;
margin-bottom: 1em !important;
}
}
.download h3 {
margin-top: 2em;
}
.download button {
margin-right: 0.5em;
margin-left: 0.5em;
}
.typo {
text-align: left;
}
.typo a {
color: #0077ff;
}
hr {
height: 10px;
margin-bottom: .8em;
border: none;
border-bottom: 1px solid rgba(0, 0, 0, .12);
nav li {
display: inline;
margin-right: 15px;
}
</style>

View File

@@ -11,14 +11,18 @@
</template>
<script setup>
import { ref,watch } from 'vue'
import { ref, watch, onMounted } from 'vue'
import { useDark, useToggle } from '@vueuse/core'
/** 引入Element-Plus图标 */
import { Sunny, Moon } from '@element-plus/icons-vue'
defineOptions({
name: 'DarkMode'
})
// 定义事件
const emit = defineEmits(['theme-change'])
/** 切换模式 */
const isDark = useDark({})
@@ -30,8 +34,32 @@ if (item) {
}
/** 是否切换为暗黑模式 */
const darkMode = ref(item)
watch(darkMode, (newValue) => {
console.log(`darkMode: ${newValue}`)
window.localStorage.setItem("darkMode", newValue);
// 发射主题变化事件
emit('theme-change', newValue)
// 应用主题到body
if (newValue) {
document.body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme')
} else {
document.body.classList.remove('dark-theme')
document.documentElement.classList.remove('dark-theme')
}
})
onMounted(() => {
// 初始化时发射当前主题状态
emit('theme-change', darkMode.value)
// 应用初始主题
if (darkMode.value) {
document.body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme')
}
})
</script>

View File

@@ -0,0 +1,993 @@
<template>
<div class="main-container">
<div class="directory-tree" :class="{ 'dark-theme': isDarkTheme }">
<template v-if="viewMode === 'pane'">
<!-- 窗格模式原有 -->
<div class="breadcrumb">
<div
v-for="(item, index) in pathStack"
:key="index"
class="breadcrumb-item"
:class="{ 'active': index === pathStack.length - 1 }"
@click="goToDirectory(index)"
>
<i class="fas fa-folder" v-if="index === 0"></i>
<i class="fas fa-chevron-right" v-else-if="index > 0"></i>
{{ item.name }}
</div>
</div>
<div class="file-grid" v-loading="loading">
<div
v-for="file in currentFileList"
:key="file.fileName"
class="file-item"
:class="getFileTypeClass(file)"
@click="handleFileClick(file)"
>
<div class="file-icon">
<i :class="getFileIcon(file)"></i>
</div>
<div class="file-name">{{ file.fileName }}</div>
<div class="file-meta">
{{ file.sizeStr || '0B' }} · {{ formatDate(file.createTime) }}
</div>
</div>
<div v-if="!loading && (!currentFileList || currentFileList.length === 0)" class="empty-state">
<i class="fas fa-folder-open"></i>
<h3>此文件夹为空</h3>
<p>暂无文件或文件夹</p>
</div>
</div>
<div class="action-bar">
<el-button
type="primary"
@click="goBack"
:disabled="pathStack.length <= 1"
icon="el-icon-arrow-left"
>
返回上一级
</el-button>
<div class="stats">
<span class="stat-item">
<i class="fas fa-folder"></i> {{ folderCount }} 个文件夹
</span>
<span class="stat-item">
<i class="fas fa-file"></i> {{ fileCount }} 个文件
</span>
</div>
</div>
</template>
<template v-else-if="viewMode === 'tree'">
<div class="content-card">
<splitpanes class="split-theme custom-splitpanes" style="height:100%;">
<pane>
<div class="tree-sidebar">
<el-tree
:data="treeData"
:props="treeProps"
node-key="id"
lazy
:load="loadNode"
highlight-current
@node-click="onNodeClick"
:default-expand-all="false"
:default-expanded-keys="['root']"
:render-content="renderContent"
style="background:transparent;"
/>
</div>
</pane>
<pane>
<div class="tree-content">
<div v-if="selectedNode">
<div class="file-detail-icon-wrap">
<i :class="getFileIcon(selectedNode)" class="file-detail-icon"></i>
</div>
<h4>{{ selectedNode.fileName }}</h4>
<div v-if="selectedNode.fileType === 'folder'">
<ul>
<li v-for="file in selectedNode.children || []" :key="file.id">
<i :class="getFileIcon(file)"></i> {{ file.fileName }}
</li>
</ul>
</div>
<div v-else>
<p>类型: {{ getFileTypeClass(selectedNode) }}</p>
<p>大小: {{ selectedNode.sizeStr || '0B' }}</p>
<p>创建时间: {{ formatDate(selectedNode.createTime) }}</p>
<!-- 文件详情区下载按钮 -->
<el-button v-if="selectedNode && selectedNode.parserUrl" @click="previewFile(selectedNode)">打开</el-button>
<a
v-if="selectedNode && selectedNode.parserUrl"
:href="selectedNode.parserUrl"
download
target="_blank"
class="el-button el-button--success"
style="margin-left: 8px;"
>
下载
</a>
</div>
</div>
<div v-else style="color: #888;">请选择左侧文件或文件夹</div>
</div>
</pane>
</splitpanes>
</div>
</template>
<!-- 文件操作对话框窗格模式下 -->
<el-dialog
v-if="viewMode === 'pane'"
title="文件操作"
v-model="fileDialogVisible"
width="400px"
:before-close="closeFileDialog"
>
<div class="file-dialog-content">
<p><strong>{{ selectedFile?.fileName || '未命名文件' }}</strong></p>
<p class="file-info">
大小: {{ selectedFile?.sizeStr || '0B' }}<br>
创建时间: {{ formatDate(selectedFile?.createTime) }}
</p>
</div>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="previewFile(selectedFile)">打开</el-button>
<!-- 弹窗下载按钮 -->
<a
v-if="selectedFile && selectedFile.parserUrl"
:href="selectedFile.parserUrl"
download
target="_blank"
class="el-button el-button--success"
style="margin-left: 8px;"
>
下载
</a>
</span>
</el-dialog>
<div v-if="isPreviewing" class="preview-mask">
<div class="preview-toolbar">
<el-button size="small" @click="closePreview">关闭预览</el-button>
<el-button size="small" type="primary" @click="openPreviewInNewTab">新窗口打开</el-button>
</div>
<iframe :src="previewUrl" frameborder="0" class="preview-iframe"></iframe>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import { ElTree } from 'element-plus'
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import fileTypeUtils from '@/utils/fileTypeUtils'
export default {
name: 'DirectoryTree',
components: { ElTree, Splitpanes, Pane },
props: {
fileList: {
type: Array,
default: () => []
},
shareUrl: {
type: String,
required: true
},
password: {
type: String,
default: ''
},
viewMode: {
type: String,
default: 'pane' // 'pane' or 'tree'
}
},
data() {
return {
loading: false,
pathStack: [{ name: '全部文件', url: '' }],
currentFileList: [],
fileDialogVisible: false,
selectedFile: null,
isDarkTheme: false,
initialized: false,
// 文件树模式相关
treeData: [],
selectedNode: null,
isPreviewing: false,
previewUrl: '',
treeProps: {
label: 'fileName',
children: 'children',
isLeaf: 'isLeaf'
}
}
},
computed: {
folderCount() {
return this.currentFileList.filter(file => file.fileType === 'folder').length
},
fileCount() {
return this.currentFileList.filter(file => file.fileType !== 'folder').length
}
},
watch: {
fileList: {
immediate: true,
handler(newList) {
// 根节点children为当前目录下所有文件/文件夹
this.treeData = [
{
id: 'root',
fileName: '全部文件',
fileType: 'folder',
children: (newList || []).map(item => ({
...item,
isLeaf: item.fileType !== 'folder'
})),
isLeaf: false
}
]
this.currentFileList = newList
}
}
},
methods: {
...fileTypeUtils,
// 构建API URL
buildApiUrl() {
const baseUrl = `${window.location.origin}/v2/getFileList`
const params = new URLSearchParams({
url: this.shareUrl
})
if (this.password) {
params.append('pwd', this.password)
}
return `${baseUrl}?${params.toString()}`
},
// 文件树与窗格同源:直接返回当前目录数据
buildTree(list) {
return list || []
},
// 懒加载子节点
loadNode(node, resolve) {
if (node.level === 0) {
// 根节点
resolve(this.treeData[0].children)
} else if (node.data.fileType === 'folder' && node.data.parserUrl) {
axios.get(node.data.parserUrl).then(res => {
if (res.data.code === 200) {
const children = (res.data.data || []).map(item => ({
...item,
isLeaf: item.fileType !== 'folder'
}))
resolve(children)
} else {
resolve([])
}
}).catch(() => resolve([]))
} else {
resolve([])
}
},
onNodeClick(data) {
this.selectedNode = data
},
// 处理文件点击
handleFileClick(file) {
console.log('点击文件', file, this.viewMode)
if (file.fileType === 'folder') {
this.enterFolder(file)
} else if (this.viewMode === 'pane') {
this.selectedFile = file
this.fileDialogVisible = true
}
},
// 进入文件夹
async enterFolder(folder) {
if (!folder.parserUrl) {
this.$message.error('无法进入该文件夹,缺少访问链接')
return
}
try {
this.loading = true
const response = await axios.get(folder.parserUrl)
if (response.data.code === 200) {
const newDir = {
url: folder.parserUrl,
name: folder.fileName || '未命名文件夹'
}
this.pathStack.push(newDir)
this.currentFileList = response.data.data || []
} else {
this.$message.error(response.data.msg || '获取文件夹内容失败')
}
} catch (error) {
console.error('进入文件夹失败:', error)
this.$message.error('进入文件夹失败')
} finally {
this.loading = false
}
},
goBack() {
if (this.pathStack.length > 1) {
this.pathStack.pop()
this.loadCurrentDirectory()
}
},
goToDirectory(index) {
this.pathStack.splice(index + 1)
this.loadCurrentDirectory()
},
async loadCurrentDirectory() {
const currentDir = this.pathStack[this.pathStack.length - 1]
if (!currentDir.url) {
this.currentFileList = this.fileList
return
}
try {
this.loading = true
const response = await axios.get(currentDir.url)
if (response.data.code === 200) {
this.currentFileList = response.data.data || []
} else {
this.$message.error(response.data.msg || '加载目录失败')
}
} catch (error) {
console.error('加载目录失败:', error)
this.$message.error('加载目录失败')
} finally {
this.loading = false
}
},
// 预览文件
previewFile(file) {
if (file?.previewUrl || file?.parserUrl) {
this.previewUrl = file.previewUrl || file.parserUrl
this.isPreviewing = true
} else {
this.$message.warning('该文件暂无预览链接')
}
this.closeFileDialog()
},
// 下载文件
downloadFile(file) {
if (file?.parserUrl) {
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = file.parserUrl
document.body.appendChild(iframe)
setTimeout(() => {
document.body.removeChild(iframe)
}, 1000)
this.$message.success('开始下载文件')
} else {
this.$message.warning('该文件暂无下载链接')
}
this.closeFileDialog()
},
closeFileDialog() {
this.fileDialogVisible = false
this.selectedFile = null
},
closePreview() {
this.isPreviewing = false
this.previewUrl = ''
},
openPreviewInNewTab() {
if (this.previewUrl) {
window.open(this.previewUrl, '_blank')
}
},
formatDate(timestamp) {
if (!timestamp) return '未知时间'
const date = new Date(timestamp)
return date.toLocaleString('zh-CN')
},
checkTheme() {
this.isDarkTheme = document.body.classList.contains('dark-theme') ||
document.documentElement.classList.contains('dark-theme')
},
renderContent(h, { node, data, store }) {
const isFolder = data.fileType === 'folder'
return h('div', {
class: 'custom-tree-node'
}, [
h('i', {
class: [this.getFileIcon(data), { 'folder-icon': isFolder, 'file-icon': !isFolder }]
}),
h('span', {
class: ['node-label', { 'folder-text': isFolder, 'file-text': !isFolder }]
}, node.label)
])
}
},
mounted() {
this.checkTheme()
this.initialized = true
// 监听主题变化
this._observer = new MutationObserver(() => {
this.checkTheme()
})
this._observer.observe(document.body, {
attributes: true,
attributeFilter: ['class']
})
},
beforeUnmount() {
if (this._observer) {
this._observer.disconnect()
}
}
}
</script>
<style>
html, body, #app, .main-container, .directory-tree, .content-card {
/* overflow: hidden; */
/* overflow: auto; */
/* position: relative; */
}
.main-container {
height: 100%;
display: flex;
flex-direction: column;
}
.directory-tree {
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
overflow: hidden;
transition: all 0.3s ease;
}
.directory-tree.dark-theme {
background: #2d2d2d;
color: #ffffff;
}
.breadcrumb {
background: #f8f9fa;
padding: 16px 24px;
border-bottom: 1px solid #eaeaea;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.dark-theme .breadcrumb {
background: #404040;
border-bottom-color: #555555;
}
.breadcrumb-item {
display: flex;
align-items: center;
font-size: 0.95rem;
color: #7f8c8d;
cursor: pointer;
transition: color 0.2s;
margin-right: 8px;
}
.breadcrumb-item:hover {
color: #3498db;
}
.breadcrumb-item.active {
color: #2c3e50;
font-weight: 600;
}
.dark-theme .breadcrumb-item.active {
color: #ffffff;
}
.breadcrumb-item i {
margin: 0 8px;
font-size: 0.8rem;
color: #bdc3c7;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
padding: 12px;
min-height: 200px;
}
.file-item {
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
cursor: pointer;
text-align: center;
padding: 10px 4px;
min-height: 80px;
border: 2px solid transparent;
}
.dark-theme .file-item {
background: #404040;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2);
}
.file-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
border-color: #3498db;
}
.dark-theme .file-item:hover {
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
}
.file-icon {
font-size: 2.2rem;
margin-bottom: 8px;
transition: transform 0.3s;
}
.file-item:hover .file-icon {
transform: scale(1.1);
}
.folder .file-icon {
color: #3498db;
}
.image .file-icon {
color: #e74c3c;
}
.document .file-icon {
color: #f39c12;
}
.archive .file-icon {
color: #9b59b6;
}
.audio .file-icon {
color: #1abc9c;
}
.video .file-icon {
color: #d35400;
}
.code .file-icon {
color: #27ae60;
}
.file-name {
font-weight: 500;
font-size: 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
margin-bottom: 4px;
}
.file-meta {
font-size: 0.75rem;
color: #95a5a6;
}
.dark-theme .file-meta {
color: #bdc3c7;
}
.empty-state {
text-align: center;
padding: 30px 10px;
color: #7f8c8d;
grid-column: 1 / -1;
}
.dark-theme .empty-state {
color: #bdc3c7;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 10px;
color: #bdc3c7;
}
.dark-theme .empty-state i {
color: #555555;
}
.empty-state h3 {
font-size: 1.1rem;
margin-bottom: 6px;
color: #2c3e50;
}
.dark-theme .empty-state h3 {
color: #ffffff;
}
.action-bar {
display: flex;
justify-content: space-between;
padding: 10px 12px;
background: #f8f9fa;
border-top: 1px solid #eaeaea;
}
.dark-theme .action-bar {
background: #404040;
border-top-color: #555555;
}
.stats {
display: flex;
align-items: center;
gap: 10px;
color: #7f8c8d;
font-size: 0.85rem;
}
.dark-theme .stats {
color: #bdc3c7;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.file-dialog-content {
text-align: center;
}
.file-info {
color: #7f8c8d;
font-size: 0.9rem;
margin-top: 10px;
}
.dark-theme .file-info {
color: #bdc3c7;
}
.tree-layout {
display: flex;
height: 500px;
}
.tree-sidebar {
width: 220px;
background: #f8f9fa;
border-right: 1px solid #eaeaea;
overflow-y: auto;
}
.directory-tree.dark-theme .tree-sidebar {
background: #232323;
border-right: 1px solid #404040;
}
.file-tree-root, .tree-node ul {
list-style: none;
padding-left: 12px;
margin: 0;
}
.tree-node {
margin-bottom: 2px;
}
.tree-node.selected > .tree-node-label {
background: #e6f7ff;
color: #409eff;
}
.directory-tree.dark-theme .tree-node.selected > .tree-node-label {
background: #333c4d;
color: #4a9eff;
}
.tree-node-label {
cursor: pointer;
padding: 3px 6px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 4px;
transition: background 0.2s;
font-size: 0.95em;
}
.tree-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
/* 自定义树节点样式 */
.custom-tree-node {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 0;
width: 100%;
}
.custom-tree-node i {
font-size: 16px;
width: 20px;
text-align: center;
color: #606266;
}
.dark-theme .custom-tree-node i {
color: #bdc3c7;
}
.custom-tree-node .node-label {
flex: 1;
font-size: 14px;
color: #303133;
}
.dark-theme .custom-tree-node .node-label {
color: #e1e1e1;
}
/* 文件夹样式 */
.custom-tree-node .folder-icon {
color: #409eff !important;
}
.dark-theme .custom-tree-node .folder-icon {
color: #4a9eff !important;
}
.custom-tree-node .folder-text {
color: #409eff !important;
font-weight: 500;
}
.dark-theme .custom-tree-node .folder-text {
color: #4a9eff !important;
}
/* 文件样式 */
.custom-tree-node .file-icon {
color: #95a5a6 !important;
}
.dark-theme .custom-tree-node .file-icon {
color: #bdc3c7 !important;
}
.custom-tree-node .file-text {
color: #606266 !important;
}
.dark-theme .custom-tree-node .file-text {
color: #e1e1e1 !important;
}
/* 特殊文件类型图标颜色 */
.custom-tree-node i.fa-file-image {
color: #e74c3c !important;
}
.custom-tree-node i.fa-file-pdf {
color: #e74c3c !important;
}
.custom-tree-node i.fa-file-word {
color: #3498db !important;
}
.custom-tree-node i.fa-file-excel {
color: #27ae60 !important;
}
.custom-tree-node i.fa-file-powerpoint {
color: #f39c12 !important;
}
.custom-tree-node i.fa-file-archive {
color: #9b59b6 !important;
}
.custom-tree-node i.fa-file-audio {
color: #1abc9c !important;
}
.custom-tree-node i.fa-file-video {
color: #d35400 !important;
}
.custom-tree-node i.fa-file-code {
color: #27ae60 !important;
}
/* 树节点悬停效果 */
.el-tree-node__content:hover .custom-tree-node {
background-color: #f5f7fa;
border-radius: 4px;
}
.dark-theme .el-tree-node__content:hover .custom-tree-node {
background-color: #2c2c2c;
}
/* 选中节点样式 */
.el-tree-node.is-current > .el-tree-node__content .custom-tree-node {
background-color: #e6f7ff;
border-radius: 4px;
}
.dark-theme .el-tree-node.is-current > .el-tree-node__content .custom-tree-node {
background-color: #333c4d;
}
.preview-mask { position: fixed; z-index: 9999; left: 0; top: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.85); display: flex; flex-direction: column; }
.preview-toolbar { padding: 12px; background: #232323; text-align: right; }
.preview-iframe { flex: 1; width: 100vw; border: none; background: #222; }
.content-card {
min-height: 500px;
height: 100%;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
margin: 0 0 12px 0;
display: flex;
flex-direction: column;
}
.dark-theme .content-card {
background: #232323;
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
}
.split-theme {
flex: 1 1 0;
height: 100%;
}
.tree-sidebar, .tree-content {
width: 100%;
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
overflow: auto;
}
.tree-content {
padding: 40px 16px 16px 16px;
align-items: flex-start;
}
.file-detail-icon-wrap {
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 12px;
}
.file-detail-icon {
font-size: 48px;
color: #409eff;
display: block;
}
.dark-theme .file-detail-icon {
color: #4a9eff;
}
/* splitpanes 拖拽条自定义按钮 */
.custom-splitpanes .splitpanes__splitter {
position: relative;
background: #e0e0e0;
transition: background 0.2s;
touch-action: pan-x pan-y;
}
.custom-splitpanes .splitpanes__splitter:hover {
background: #b3b3b3;
}
.custom-splitpanes .splitpanes__splitter:after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 28px;
height: 28px;
border-radius: 50%;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border: 1.5px solid #d0d0d0;
z-index: 2;
display: block;
}
.dark-theme .custom-splitpanes .splitpanes__splitter:after {
background: #232323;
border-color: #444;
}
.feedback-bar {
width: 100%;
text-align: right;
padding: 12px 18px 0 0;
}
.feedback-link {
color: #e74c3c;
font-weight: bold;
font-size: 1.08rem;
text-decoration: none;
border: 1px solid #e74c3c;
border-radius: 6px;
padding: 4px 14px;
background: #fff5f5;
transition: background 0.2s, color 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 10px;
}
.feedback-link:first-child { margin-left: 0; }
.feedback-link:hover {
background: #e74c3c;
color: #fff;
}
.dark-theme .feedback-link {
background: #2d2d2d;
color: #ff7675;
border-color: #ff7675;
}
.dark-theme .feedback-link:hover {
background: #ff7675;
color: #232323;
}
.feedback-icon {
font-size: 1.15em;
color: #e74c3c;
margin-right: 2px;
}
.feedback-link:hover .feedback-icon {
color: #fff;
}
.feedback-link:nth-child(2) .feedback-icon { color: #333; }
.feedback-link:nth-child(3) .feedback-icon { color: #f39c12; }
.dark-theme .feedback-icon {
color: #ff7675;
}
.dark-theme .feedback-link:nth-child(2) .feedback-icon { color: #fff; }
.dark-theme .feedback-link:nth-child(3) .feedback-icon { color: #f7ca77; }
@media (max-width: 768px) {
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 8px;
padding: 6px;
}
.file-item {
padding: 6px 2px;
min-height: 60px;
}
.file-icon {
font-size: 1.5rem;
}
}
@media (max-width: 480px) {
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 4px;
}
.action-bar {
flex-direction: column;
gap: 6px;
}
}
</style>

View File

@@ -9,22 +9,24 @@ import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import "vue3-json-viewer/dist/index.css";
import './styles/dark/css-vars.css'
import router from './router/index.js'
window.$vueApp = Vue.createApp(App)
const app = Vue.createApp(App)
app.use(router)
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
window.$vueApp.component(key, component)
app.component(key, component)
}
// Import JsonViewer as a Vue.js plugin
window.$vueApp.use(JsonViewer)
window.$vueApp.use(DirectiveExtensions)
app.use(JsonViewer)
app.use(DirectiveExtensions)
// or
// components: {JsonViewer}
window.$vueApp.use(VueClipboard)
window.$vueApp.use(ElementPlus)
window.$vueApp.mount('#app')
app.use(VueClipboard)
app.use(ElementPlus)
app.mount('#app')

View File

@@ -1,10 +1,5 @@
// 修改自 https://github.com/syhyz1990/panAI
const util = {
isMobile: (() => !!navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone|HarmonyOS|MicroMessenger)/i))(),
}
let opt = {
// 'baidu': {
@@ -293,6 +288,12 @@
host: /mail\.qq\.com/,
name: 'QQ邮箱中转站'
},
QQsc: {
// qfile.qq.com
reg: /https:\/\/qfile\.qq\.com\/q\/.+/,
host: /qfile\.qq\.com/,
name: 'QQ闪传'
},
pan118: {
reg: /https:\/\/(?:[a-zA-Z\d-]+\.)?118pan\.com\/b.+/,
host: /118pan\.com/,

View File

@@ -0,0 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import ShowFile from '@/views/ShowFile.vue'
import ShowList from '@/views/ShowList.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/showFile', component: ShowFile },
{ path: '/showList', component: ShowList }
]
const router = createRouter({
history: createWebHistory('/'),
routes
})
export default router

View File

@@ -0,0 +1,164 @@
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: all 0.3s ease;
}
/* 亮色主题 */
body {
background-color: #f5f7fa;
color: #2c3e50;
}
/* 暗色主题 */
body.dark-theme {
background-color: #1a1a1a;
color: #ffffff;
}
/* Element Plus 暗色主题适配 */
.dark-theme .el-card {
background-color: #2d2d2d !important;
border-color: #404040 !important;
color: #ffffff !important;
}
.dark-theme .el-input__inner {
background-color: #404040 !important;
border-color: #555555 !important;
color: #ffffff !important;
}
.dark-theme .el-input__inner::placeholder {
color: #bdc3c7 !important;
}
.dark-theme .el-button {
background-color: #404040 !important;
border-color: #555555 !important;
color: #ffffff !important;
}
.dark-theme .el-button:hover {
background-color: #555555 !important;
}
.dark-theme .el-button--primary {
background-color: #409eff !important;
border-color: #409eff !important;
}
.dark-theme .el-button--primary:hover {
background-color: #66b1ff !important;
}
.dark-theme .el-button--success {
background-color: #67c23a !important;
border-color: #67c23a !important;
}
.dark-theme .el-button--success:hover {
background-color: #85ce61 !important;
}
.dark-theme .el-switch__core {
background-color: #555555 !important;
}
.dark-theme .el-descriptions {
background-color: #2d2d2d !important;
color: #ffffff !important;
}
.dark-theme .el-descriptions__label {
color: #bdc3c7 !important;
}
.dark-theme .el-descriptions__content {
color: #ffffff !important;
}
.dark-theme .el-dialog {
background-color: #2d2d2d !important;
color: #ffffff !important;
}
.dark-theme .el-dialog__title {
color: #ffffff !important;
}
.dark-theme .el-dialog__body {
color: #ffffff !important;
}
.dark-theme .el-message {
background-color: #2d2d2d !important;
color: #ffffff !important;
}
.dark-theme .el-message--success {
background-color: #67c23a !important;
color: #ffffff !important;
}
.dark-theme .el-message--error {
background-color: #f56c6c !important;
color: #ffffff !important;
}
.dark-theme .el-message--warning {
background-color: #e6a23c !important;
color: #ffffff !important;
}
.dark-theme .el-message--info {
background-color: #909399 !important;
color: #ffffff !important;
}
/* 链接颜色 */
.dark-theme a {
color: #4a9eff !important;
}
.dark-theme a:hover {
color: #66b1ff !important;
}
/* 滚动条样式 */
.dark-theme ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.dark-theme ::-webkit-scrollbar-track {
background: #2d2d2d;
}
.dark-theme ::-webkit-scrollbar-thumb {
background: #555555;
border-radius: 4px;
}
.dark-theme ::-webkit-scrollbar-thumb:hover {
background: #666666;
}
/* 选择文本样式 */
.dark-theme ::selection {
background-color: #409eff;
color: #ffffff;
}
.dark-theme ::-moz-selection {
background-color: #409eff;
color: #ffffff;
}

View File

@@ -0,0 +1,85 @@
const fileTypeUtils = {
getFileExtension(filename) {
if (!filename) return ''
return filename.split('.').pop()
},
getFileTypeClass(file) {
if (file.fileType === 'folder') return 'folder'
const ext = this.getFileExtension(file.fileName)
const fileTypes = {
'image': ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'],
'document': ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf'],
'archive': ['zip', 'rar', '7z', 'tar', 'gz'],
'audio': ['mp3', 'wav', 'ogg', 'flac'],
'video': ['mp4', 'avi', 'mov', 'wmv', 'mkv', 'flv'],
'code': ['html', 'htm', 'css', 'js', 'json', 'php', 'py', 'java', 'c', 'cpp', 'h', 'sh', 'bat', 'md']
}
for (const [type, extensions] of Object.entries(fileTypes)) {
if (extensions.includes(ext.toLowerCase())) {
return type
}
}
return 'document'
},
getFileIcon(file) {
if (file.fileType === 'folder') return 'fas fa-folder'
const ext = this.getFileExtension(file.fileName)
const iconMap = {
'jpg': 'fas fa-file-image', 'jpeg': 'fas fa-file-image', 'png': 'fas fa-file-image',
'gif': 'fas fa-file-image', 'bmp': 'fas fa-file-image', 'svg': 'fas fa-file-image', 'webp': 'fas fa-file-image',
'pdf': 'fas fa-file-pdf', 'doc': 'fas fa-file-word', 'docx': 'fas fa-file-word',
'xls': 'fas fa-file-excel', 'xlsx': 'fas fa-file-excel', 'ppt': 'fas fa-file-powerpoint', 'pptx': 'fas fa-file-powerpoint',
'txt': 'fas fa-file-alt', 'rtf': 'fas fa-file-alt',
'zip': 'fas fa-file-archive', 'rar': 'fas fa-file-archive', '7z': 'fas fa-file-archive',
'tar': 'fas fa-file-archive', 'gz': 'fas fa-file-archive',
'mp3': 'fas fa-file-audio', 'wav': 'fas fa-file-audio', 'ogg': 'fas fa-file-audio', 'flac': 'fas fa-file-audio',
'mp4': 'fas fa-file-video', 'avi': 'fas fa-file-video', 'mov': 'fas fa-file-video',
'wmv': 'fas fa-file-video', 'mkv': 'fas fa-file-video', 'flv': 'fas fa-file-video',
'html': 'fas fa-file-code', 'htm': 'fas fa-file-code', 'css': 'fas fa-file-code',
'js': 'fas fa-file-code', 'json': 'fas fa-file-code', 'php': 'fas fa-file-code',
'py': 'fas fa-file-code', 'java': 'fas fa-file-code', 'c': 'fas fa-file-code',
'cpp': 'fas fa-file-code', 'h': 'fas fa-file-code', 'sh': 'fas fa-file-code',
'bat': 'fas fa-file-code', 'md': 'fas fa-file-code'
}
return iconMap[ext.toLowerCase()] || 'fas fa-file'
},
extractFileNameAndExt(url) {
if (!url) return { name: '', ext: '' }
const filenameParams = [
'response-content-disposition', 'filename', 'filename*', 'fn', 'fname', 'download_name'
];
let name = null;
try {
const u = new URL(url, window.location.origin);
for (const param of filenameParams) {
const value = u.searchParams.get(param);
if (value) {
if (param === 'response-content-disposition') {
const match = value.match(/filename\*?=(.*'')?(?<FN>.*)/i);
name = match && match.groups && match.groups['FN'] ? match.groups['FN'] : value;
} else {
name = value;
}
break;
}
}
if (name) {
name = decodeURIComponent(name).replace(/['"]/g, '');
} else {
const decodedUrl = decodeURIComponent(url);
const paths = decodedUrl.split('/');
name = paths[paths.length - 1].split('?')[0];
}
let ext = '';
if (name) {
const spl = name.split('.');
ext = spl.length > 1 ? spl[spl.length - 1].toLowerCase() : '';
}
return { name, ext };
} catch {
return { name: '', ext: '' }
}
}
}
export default fileTypeUtils

View File

@@ -0,0 +1,884 @@
<template>
<div id="app" v-cloak :class="{ 'dark-theme': isDarkMode }">
<!-- <el-dialog
v-model="showRiskDialog"
title="使用本网站您应改同意"
width="300px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
center
>
<div style="font-size:1.08em;line-height:1.8;">
请勿在本平台分享传播任何违法内容包括但不限于<br>
违规视频游戏外挂侵权资源涉政涉黄等<br>
</div>
<template #footer>
<el-button type="primary" @click="ackRisk">我知道了</el-button>
</template>
</el-dialog> -->
<!-- 顶部反馈栏小号灰色无红边框 -->
<div class="feedback-bar">
<a href="https://github.com/qaiu/netdisk-fast-download/issues" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-bug feedback-icon"></i>
反馈
</a>
<a href="https://github.com/qaiu/netdisk-fast-download" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fab fa-github feedback-icon"></i>
源码
</a>
<a href="https://blog.qaiu.top" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-blog feedback-icon"></i>
博客
</a>
<a href="https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-server feedback-icon"></i>
部署
</a>
</div>
<el-row :gutter="20" style="margin-left: 0; margin-right: 0;">
<el-card class="box-card">
<div style="text-align: right">
<DarkMode @theme-change="handleThemeChange" />
</div>
<div class="demo-basic--circle">
<div class="block" style="text-align: center;">
<img :height="150" src="../../public/images/lanzou111.png" alt="lz">
</div>
</div>
<!-- 项目简介移到卡片内 -->
<div class="project-intro">
<div class="intro-title">NFD网盘直链解析0.1.9_bate8</div>
<div class="intro-desc">
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘奶牛快传移动云空间QQ邮箱云盘QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> &gt;&gt; </el-link></div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
</div>
</div>
<div class="typo">
<p>节点1: 回源请求数:{{ node1Info.parserTotal }}, 缓存请求数:{{ node1Info.cacheTotal }}, 总数:{{ node1Info.total }}</p>
</div>
<hr>
<div class="main" v-loading="isLoading">
<div class="grid-content">
<!-- 开关按钮控制是否自动读取剪切板 -->
<el-switch v-model="autoReadClipboard" active-text="自动识别剪切板"></el-switch>
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url">
<template #prepend>分享链接</template>
<template #append v-if="!autoReadClipboard">
<el-button @click="getPaste(true)">读取剪切板</el-button>
</template>
</el-input>
<el-input placeholder="请输入密码" v-model="password" id="url">
<template #prepend>分享密码</template>
</el-input>
<el-input v-show="directLink" :value="directLink" id="url">
<template #prepend>智能直链</template>
<template #append>
<el-button v-clipboard:copy="directLink" v-clipboard:success="onCopy" v-clipboard:error="onError">
<el-icon><CopyDocument /></el-icon>
</el-button>
</template>
</el-input>
<p style="text-align: center">
<el-button style="margin-left: 40px" @click="parseFile">解析文件</el-button>
<el-button style="margin-left: 20px" @click="parseDirectory">解析目录</el-button>
<el-button style="margin-left: 20px" @click="generateMarkdown">生成Markdown</el-button>
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
<el-button style="margin-left: 20px" @click="getStatistics">分享统计</el-button>
</p>
</div>
<!-- 解析结果 -->
<div v-if="parseResult.code" style="margin-top: 10px">
<strong>解析结果: </strong>
<json-viewer :value="parseResult" :expand-depth="5" copyable boxed sort />
<!-- 文件信息美化展示区 -->
<div v-if="downloadUrl" class="file-meta-info-card">
<div class="file-meta-row">
<span class="file-meta-label">下载链接</span>
<a :href="downloadUrl" target="_blank" class="file-meta-link" rel="noreferrer noopener">点击下载</a>
</div>
<div class="file-meta-row" v-if="parseResult.data?.downloadShortUrl">
<span class="file-meta-label">下载短链</span>
<a :href="parseResult.data.downloadShortUrl" target="_blank" class="file-meta-link">{{ parseResult.data.downloadShortUrl }}</a>
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件预览</span>
<a :href="previewBaseUrl + encodeURIComponent(downloadUrl)" target="_blank" class="file-meta-link">点击预览</a>
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件名</span>{{ extractFileNameAndExt(downloadUrl).name }}
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件类型</span>{{ getFileTypeClass({ fileName: extractFileNameAndExt(downloadUrl).name }) }}
</div>
<div class="file-meta-row" v-if="parseResult.data?.sizeStr">
<span class="file-meta-label">文件大小</span>{{ parseResult.data.sizeStr }}
</div>
</div>
</div>
<!-- Markdown链接 -->
<div v-if="markdownText" style="text-align: center">
<el-input :value="markdownText" readonly>
<template #append>
<el-button v-clipboard:copy="markdownText" v-clipboard:success="onCopy" v-clipboard:error="onError">
<el-icon><CopyDocument /></el-icon>
</el-button>
</template>
</el-input>
</div>
<!-- 二维码 -->
<div style="text-align: center" v-show="showQRCode">
<canvas ref="qrcodeCanvas"></canvas>
<div style="text-align: center">
<el-link target="_blank" :href="qrCodeUrl">{{ qrCodeUrl }}</el-link>
</div>
</div>
<!-- 统计信息 -->
<div v-if="statisticsData.shareLinkInfo">
<el-descriptions class="margin-top" title="分享详情" :column="1" border>
<template slot="extra">
<el-button type="primary" size="small">操作</el-button>
</template>
<el-descriptions-item label="网盘名称">{{ statisticsData.shareLinkInfo.panName }}</el-descriptions-item>
<el-descriptions-item label="网盘标识">{{ statisticsData.shareLinkInfo.type }}</el-descriptions-item>
<el-descriptions-item label="分享Key">{{ statisticsData.shareLinkInfo.shareKey }}</el-descriptions-item>
<el-descriptions-item label="分享链接">
<el-link target="_blank" :href="statisticsData.shareLinkInfo.shareUrl">{{ statisticsData.shareLinkInfo.shareUrl }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="jsonApi链接">
<el-link target="_blank" :href="statisticsData.apiLink">{{ statisticsData.apiLink }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="302下载链接">
<el-link target="_blank" :href="statisticsData.downLink">{{ statisticsData.downLink }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="302预览链接">
<el-link target="_blank" :href="statisticsData.viewLink">{{ statisticsData.viewLink }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="解析次数">{{ statisticsData.parserTotal }}</el-descriptions-item>
<el-descriptions-item label="缓存命中次数">{{ statisticsData.cacheHitTotal }}</el-descriptions-item>
<el-descriptions-item label="总请求次数">{{ statisticsData.sumTotal }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 错误时显示小按钮 -->
<div v-if="errorButtonVisible" style="text-align: center; margin-top: 10px;">
<el-button type="text" @click="errorDialogVisible = true"> 反馈错误详情>> </el-button>
</div>
<!-- 错误 JSON 弹窗 -->
<el-dialog
v-model="errorDialogVisible"
width="60%">
<template #title>
错误详情
<el-link
@click.prevent="copyErrorDetails"
target="_blank"
style="margin-left:8px"
type="primary">
复制当前错误信息提交Issue
</el-link>
</template>
<json-viewer :value="errorDetail" :expand-depth="5" copyable boxed sort />
<template #footer>
<el-button @click="errorDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 目录树组件 -->
<div v-if="showDirectoryTree" class="directory-tree-container">
<div style="margin-bottom: 10px; text-align: right;">
<el-radio-group v-model="directoryViewMode" size="small">
<el-radio-button label="pane">窗格</el-radio-button>
<el-radio-button label="tree">文件树</el-radio-button>
</el-radio-group>
</div>
<DirectoryTree
:file-list="directoryData"
:share-url="link"
:password="password"
:view-mode="directoryViewMode"
@file-click="handleFileClick"
/>
</div>
</div>
</el-card>
</el-row>
<!-- 文件解析结果区下方加分享按钮 -->
<!-- <div v-if="parseResult.code && downloadUrl" style="margin-top: 10px; text-align: right;">-->
<!-- <el-button type="primary" @click="copyShowFileLink">分享文件直链</el-button>-->
<!-- </div>-->
<!-- 目录解析结果区下方加分享按钮 -->
<!-- <div v-if="showDirectoryTree && directoryData.length" style="margin-top: 10px; text-align: right;">-->
<!-- <el-input :value="showListLink" readonly style="width: 350px; margin-right: 10px;">-->
<!-- <template #append>-->
<!-- <el-button v-clipboard:copy="showListLink" v-clipboard:success="onCopy" v-clipboard:error="onError">-->
<!-- <el-icon><CopyDocument /></el-icon>复制分享链接-->
<!-- </el-button>-->
<!-- </template>-->
<!-- </el-input>-->
<!-- </div>-->
</div>
</template>
<script>
import axios from 'axios'
import QRCode from 'qrcode'
import DarkMode from '@/components/DarkMode'
import DirectoryTree from '@/components/DirectoryTree'
import parserUrl from '../parserUrl1'
import fileTypeUtils from '@/utils/fileTypeUtils'
import { ElMessage } from 'element-plus'
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
export default {
name: 'App',
components: { DarkMode, DirectoryTree },
mixins: [fileTypeUtils],
data() {
return {
baseAPI: `${location.protocol}//${location.host}`,
autoReadClipboard: true,
isDarkMode: false,
isLoading: false,
// 输入数据
link: "",
password: "",
// 解析结果
parseResult: {},
downloadUrl: null,
directLink: '',
previewBaseUrl,
// 功能结果
markdownText: '',
showQRCode: false,
qrCodeUrl: '',
statisticsData: {},
// 目录树
showDirectoryTree: false,
directoryData: [],
// 统计信息
node1Info: {},
node2Info: {},
hasWarnedNoLink: false,
directoryViewMode: 'pane', // 新增,目录树展示模式
hasClipboardSuccessTip: false, // 新增,聚焦期间只提示一次
showRiskDialog: false,
baseUrl: location.origin,
showListLink: '',
errorDialogVisible: false,
errorDetail: null,
errorButtonVisible: false
}
},
methods: {
// 主题切换
handleThemeChange(isDark) {
this.isDarkMode = isDark
document.body.classList.toggle('dark-theme', isDark)
window.localStorage.setItem('isDarkMode', isDark)
},
// 验证输入
validateInput() {
this.clearResults()
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) {
this.$message.error("请输入有效链接!")
throw new Error('请输入有效链接')
}
},
// 清除结果
clearResults() {
this.parseResult = {}
this.downloadUrl = null
this.markdownText = ''
this.showQRCode = false
this.statisticsData = {}
this.showDirectoryTree = false
this.directoryData = []
},
// 统一API调用
async callAPI(endpoint, params = {}) {
this.errorButtonVisible = false
try {
this.isLoading = true
const response = await axios.get(`${this.baseAPI}${endpoint}`, { params })
if (response.data.code === 200) {
// this.$message.success(response.data.msg || '操作成功')
return response.data
} else {
// 在页面右下角显示一个“查看详情”按钮 可以查看原json
this.errorDetail = response?.data
this.errorButtonVisible = true
throw new Error(response.data.msg || '操作失败')
}
} catch (error) {
this.$message.error(error.message || '网络错误')
throw error
} finally {
this.isLoading = false
}
},
// 文件解析
async parseFile() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/json/parser', params)
this.parseResult = result
this.downloadUrl = result.data?.directLink
this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}`
this.$message.success('文件解析成功!')
} catch (error) {
console.error('文件解析失败:', error)
}
},
// 目录解析
async parseDirectory() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/v2/linkInfo', params)
const data = result.data
// 检查是否支持目录解析
const supportedPans = ["iz", "lz", "fj", "ye"]
if (!supportedPans.includes(data.shareLinkInfo.type)) {
this.$message.error("当前网盘不支持目录解析")
return
}
// 获取目录数据
const directoryResult = await this.callAPI('/v2/getFileList', params)
this.directoryData = directoryResult.data || []
this.showDirectoryTree = true
// 自动赋值分享链接
this.showListLink = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}`
this.$message.success(`目录解析成功!共找到 ${this.directoryData.length} 个文件/文件夹`)
} catch (error) {
console.error('目录解析失败:', error)
}
},
// 生成Markdown
async generateMarkdown() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/v2/linkInfo', params)
this.markdownText = this.buildMarkdown('快速下载地址', result.data.downLink)
this.$message.success('Markdown生成成功')
} catch (error) {
console.error('生成Markdown失败:', error)
}
},
// 生成二维码
async generateQRCode() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/v2/linkInfo', params)
this.qrCodeUrl = result.data.downLink
const options = {
width: 150,
height: 150,
margin: 2
}
QRCode.toCanvas(this.$refs.qrcodeCanvas, this.qrCodeUrl, options, error => {
if (error) console.error(error)
})
this.showQRCode = true
this.$message.success('二维码生成成功!')
} catch (error) {
console.error('生成二维码失败:', error)
}
},
// 获取统计信息
async getStatistics() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/v2/linkInfo', params)
this.statisticsData = result.data
this.$message.success('统计信息获取成功!')
} catch (error) {
console.error('获取统计信息失败:', error)
}
},
// 构建Markdown链接
buildMarkdown(title, url) {
return `[${title}](${url})`
},
// 复制成功
onCopy() {
this.$message.success('复制成功')
},
// 复制失败
onError() {
this.$message.error('复制失败')
},
// 文件点击处理
handleFileClick(file) {
if (file.parserUrl) {
window.open(file.parserUrl, '_blank')
} else {
this.$message.warning('该文件暂无下载链接')
}
},
// 获取剪切板内容
async getPaste(isManual = false) {
try {
const text = await navigator.clipboard.readText()
console.log('获取到的文本内容是:', text)
const linkInfo = parserUrl.parseLink(text)
const pwd = parserUrl.parsePwd(text) || ''
if (linkInfo.link) {
if (linkInfo.link !== this.link || pwd !== this.password) {
this.password = pwd
this.link = linkInfo.link
this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}`
// 聚焦期间只提示一次
if (!this.hasClipboardSuccessTip) {
this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`)
this.hasClipboardSuccessTip = true
}
} else {
this.$message.warning(`[${linkInfo.name}]分享信息无变化`)
}
this.hasWarnedNoLink = false // 有效链接后重置
} else {
if (isManual || !this.hasWarnedNoLink) {
this.$message.warning("未能提取到分享链接, 该分享可能尚未支持, 你可以复制任意网盘/音乐App的分享到该页面, 系统智能识别")
this.hasWarnedNoLink = true
}
}
} catch (error) {
console.error('读取剪切板失败:', error)
this.$message.error('读取剪切板失败,请检查浏览器权限')
}
},
// 获取统计信息
async getInfo() {
try {
const response = await axios.get('/v2/statisticsInfo')
if (response.data.success) {
this.node1Info = response.data.data
}
} catch (error) {
console.error('获取统计信息失败:', error)
}
},
// 新增切换目录树展示模式方法
setDirectoryViewMode(mode) {
this.directoryViewMode = mode
},
// 文件名和类型提取方法(复用 DirectoryTree 的静态方法)
extractFileNameAndExt(url) {
return fileTypeUtils.extractFileNameAndExt(url)
},
getFileTypeClass(file) {
return fileTypeUtils.getFileTypeClass(file)
},
ackRisk() {
window.localStorage.setItem('nfd_risk_ack', '1')
this.showRiskDialog = false
},
copyShowFileLink() {
const url = `${this.baseUrl}/showFile?url=${encodeURIComponent(this.downloadUrl)}`
navigator.clipboard.writeText(url).then(() => {
ElMessage.success('文件分享链接已复制!')
})
},
copyShowListLink() {
const url = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}`
navigator.clipboard.writeText(url).then(() => {
ElMessage.success('目录分享链接已复制!')
})
},
copyErrorDetails() {
const text = `分享链接:${this.link}
分享密码:${this.password || ''}
错误信息:${JSON.stringify(this.errorDetail, null, 2)}`;
navigator.clipboard.writeText(text).then(() => {
this.$message.success('已复制分享信息和错误详情');
window.open('https://github.com/qaiu/netdisk-fast-download/issues/new', '_blank');
}).catch(() => {
this.$message.error('复制失败');
});
}
},
mounted() {
// 从localStorage读取设置
const savedAutoRead = window.localStorage.getItem("autoReadClipboard")
if (savedAutoRead !== null) {
this.autoReadClipboard = savedAutoRead === 'true'
}
// 获取初始统计信息
this.getInfo()
// 自动读取剪切板
if (this.autoReadClipboard) {
this.getPaste()
}
// 监听窗口焦点事件
window.addEventListener('focus', () => {
if (this.autoReadClipboard) {
this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次
this.getPaste()
}
})
// 首次打开页面弹出风险提示
if (!window.localStorage.getItem('nfd_risk_ack')) {
this.showRiskDialog = true
}
},
watch: {
downloadUrl(val) {
if (!val) {
this.$router.push('/')
}
},
autoReadClipboard(val) {
window.localStorage.setItem("autoReadClipboard", val)
}
}
}
</script>
<style>
[v-cloak] { display: none; }
body {
background-color: #f5f7fa;
color: #2c3e50;
margin: 0;
padding: 0;
}
body.dark-theme {
background-color: #181818;
color: #ffffff;
}
#app {
/* 不设置 background-color */
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin: auto;
padding: 1em;
max-width: 900px;
}
#app.dark-theme {
/* 不设置 background-color */
color: #ffffff;
}
.box-card {
flex: 1;
margin-top: 4em !important;
margin-bottom: 4em !important;
opacity: 1 !important; /* 只要不透明 */
background: #fff;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
border: none;
}
#app.dark-theme .box-card {
background: #232323 !important;
color: #fff !important;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
border: none;
}
@media screen and (max-width: 700px) {
#app {
padding-left: 0 !important;
padding-right: 0 !important;
margin: 0 !important; /* 关键:去掉 auto 居中 */
max-width: 100vw !important;
}
#app .box-card {
margin: 1em 4px !important; /* 上下1em左右4px */
width: auto !important;
max-width: 100vw !important;
box-sizing: border-box;
}
}
.grid-content {
margin-top: 1em;
border-radius: 4px;
min-height: 50px;
}
.el-select .el-input {
width: 130px;
}
.directory-tree-container {
margin-top: 20px;
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafafa;
}
#app.dark-theme .directory-tree-container {
background-color: #2d2d2d;
border-color: #404040;
}
.download h3 {
margin-top: 2em;
}
.download button {
margin-right: 0.5em;
margin-left: 0.5em;
}
.typo {
text-align: left;
}
.typo a {
color: #0077ff;
}
#app.dark-theme .typo a {
color: #4a9eff;
}
hr {
height: 10px;
margin-bottom: .8em;
border: none;
border-bottom: 1px solid rgba(0, 0, 0, .12);
}
#app.dark-theme hr {
border-bottom-color: rgba(255, 255, 255, .12);
}
.feedback-bar {
width: 100%;
text-align: right;
padding: 10px 10px 0 0;
}
.feedback-link {
color: #888;
font-weight: 500;
font-size: 0.98rem;
text-decoration: none;
border: none;
border-radius: 5px;
padding: 2px 10px;
background: transparent;
transition: background 0.2s, color 0.2s;
display: inline-flex;
align-items: center;
gap: 5px;
margin-left: 6px;
}
.feedback-link:first-child { margin-left: 0; }
.feedback-link:hover {
background: #f0f0f0;
color: #333;
}
.dark-theme .feedback-link {
background: transparent;
color: #bbb;
border: none;
}
.dark-theme .feedback-link:hover {
background: #333;
color: #fff;
}
.feedback-link.mini {
font-size: 0.92rem;
padding: 2px 8px;
border-radius: 4px;
}
.feedback-icon {
font-size: 1em;
color: #888;
margin-right: 2px;
}
.feedback-link:hover .feedback-icon {
color: #333;
}
.feedback-link:nth-child(2) .feedback-icon { color: #333; }
.feedback-link:nth-child(3) .feedback-icon { color: #f39c12; }
.dark-theme .feedback-icon {
color: #bbb;
}
.dark-theme .feedback-link:nth-child(2) .feedback-icon { color: #fff; }
.dark-theme .feedback-link:nth-child(3) .feedback-icon { color: #f7ca77; }
.feedback-link:nth-child(4) .feedback-icon { color: #409eff; }
.dark-theme .feedback-link:nth-child(4) .feedback-icon { color: #4a9eff; }
.project-intro {
margin: 0 auto 18px auto;
max-width: 700px;
text-align: center;
color: #888;
font-size: 1.02rem;
}
.intro-title {
font-size: 1.18rem;
font-weight: bold;
margin-bottom: 4px;
color: #666;
}
.intro-desc {
color: #888;
font-size: 0.98rem;
line-height: 1.7;
}
.dark-theme .project-intro, .dark-theme .intro-title, .dark-theme .intro-desc {
color: #bbb;
}
.dark-theme .intro-title {
color: #eee;
}
.file-meta-info-card {
margin: 18px auto 0 auto;
max-width: 600px;
background: #f8f9fa;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 18px 24px 12px 24px;
font-size: 1.02rem;
color: #333;
}
#app.dark-theme .file-meta-info-card {
background: #232323;
color: #eee;
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
}
.file-meta-row {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 1.01em;
}
.file-meta-label {
min-width: 90px;
color: #888;
font-weight: 500;
margin-right: 8px;
}
#app.dark-theme .file-meta-label {
color: #bbb;
}
.file-meta-link {
color: #409eff;
word-break: break-all;
text-decoration: underline;
}
#app.dark-theme .file-meta-link {
color: #4a9eff;
}
#app.dark-theme .jv-container {
background: #232323 !important;
color: #eee !important;
border-color: #444 !important;
}
#app.dark-theme .jv-key {
color: #4a9eff !important;
}
#app.dark-theme .jv-number {
color: #f39c12 !important;
}
#app.dark-theme .jv-string {
color: #27ae60 !important;
}
#app.dark-theme .jv-boolean {
color: #e67e22 !important;
}
#app.dark-theme .jv-null {
color: #e74c3c !important;
}
#app.jv-container .jv-item.jv-object {
color: #32ba6d;
}
.feedback-bar {
width: 100%;
margin: 0 auto; /* 居中 */
text-align: right;
box-sizing: border-box;
}
@media screen and (max-width: 700px) {
.feedback-bar {
max-width: 480px; /* 和移动端卡片宽度一致 */
padding-right: 8px; /* 和卡片内容对齐 */
padding-left: 8px;
}
}
.jv-container.jv-light .jv-item.jv-object {
color: #888;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="show-file-page">
<div v-if="loading" style="text-align:center;margin-top:40px;">加载中...</div>
<div v-else-if="error" style="color:red;text-align:center;margin-top:40px;">{{ error }}</div>
<div v-else>
<div v-if="parseResult.code">
<div class="file-meta-info-card">
<div class="file-meta-row">
<span class="file-meta-label">下载链接</span>
<a :href="downloadUrl" target="_blank" class="file-meta-link">点击下载</a>
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件名</span>{{ fileTypeUtils.extractFileNameAndExt(downloadUrl).name }}
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件类型</span>{{ fileTypeUtils.getFileTypeClass({ fileName: fileTypeUtils.extractFileNameAndExt(downloadUrl).name }) }}
</div>
<div class="file-meta-row" v-if="parseResult.data?.sizeStr">
<span class="file-meta-label">文件大小</span>{{ parseResult.data.sizeStr }}
</div>
<div class="file-meta-row">
<span class="file-meta-label">在线预览</span>
<a :href="previewBaseUrl + encodeURIComponent(downloadUrl)" target="_blank" class="preview-btn">点击在线预览</a>
</div>
</div>
</div>
<div v-else style="text-align:center;margin-top:40px;">未获取到有效解析结果</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import fileTypeUtils from '@/utils/fileTypeUtils'
import { previewBaseUrl } from '@/views/Home.vue'
export default {
name: 'ShowFile',
data() {
return {
loading: true,
error: '',
parseResult: {},
downloadUrl: '',
fileTypeUtils,
previewBaseUrl
}
},
methods: {
async fetchFile() {
const url = this.$route.query.url
if (!url) {
this.error = '缺少 url 参数'
this.loading = false
return
}
try {
const res = await axios.get('/json/parser', { params: { url } })
this.parseResult = res.data
this.downloadUrl = res.data.data?.directLink
} catch (e) {
this.error = '解析失败'
} finally {
this.loading = false
}
}
},
mounted() {
this.fetchFile()
}
}
</script>
<style scoped>
.show-file-page {
max-width: 600px;
margin: 40px auto;
}
.file-meta-info-card {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 18px 24px 12px 24px;
font-size: 1.02rem;
color: #333;
}
.file-meta-row {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 1.01em;
}
.file-meta-label {
min-width: 90px;
color: #888;
font-weight: 500;
margin-right: 8px;
}
.file-meta-link {
color: #409eff;
word-break: break-all;
text-decoration: underline;
}
.preview-btn {
display: inline-block;
padding: 4px 16px;
background: #409eff;
color: #fff;
border-radius: 5px;
text-decoration: none;
font-weight: 500;
margin-left: 8px;
transition: background 0.2s;
}
.preview-btn:hover {
background: #1867c0;
}
</style>

View File

@@ -0,0 +1,107 @@
<template>
<div class="show-list-page">
<div class="list-title-wrap">
<h2 class="list-title">{{ url }} 目录</h2>
<div class="list-subtitle">
<a :href="url" target="_blank">原始分享链接</a>
</div>
</div>
<div style="text-align:right;margin-bottom:12px;">
<DarkMode @theme-change="toggleTheme" style="float: left;"/>
<el-radio-group v-model="viewMode" size="small" style="margin-left:20px;">
<el-radio-button label="pane">窗格</el-radio-button>
<el-radio-button label="tree">目录树</el-radio-button>
</el-radio-group>
</div>
<div v-if="loading" style="text-align:center;margin-top:40px;">加载中...</div>
<div v-else-if="error" style="color:red;text-align:center;margin-top:40px;">{{ error }}</div>
<div v-else>
<DirectoryTree
:file-list="directoryData"
:share-url="url"
:password="''"
:view-mode="viewMode"
/>
</div>
</div>
</template>
<script>
import axios from 'axios'
import DirectoryTree from '@/components/DirectoryTree'
import DarkMode from '@/components/DarkMode'
export default {
name: 'ShowList',
components: { DirectoryTree, DarkMode },
data() {
return {
loading: true,
error: '',
directoryData: [],
url: '',
viewMode: 'pane'
}
},
methods: {
async fetchList() {
this.url = this.$route.query.url
if (!this.url) {
this.error = '缺少 url 参数'
this.loading = false
return
}
try {
const res = await axios.get('/v2/getFileList', { params: { url: this.url } })
this.directoryData = res.data.data || []
} catch (e) {
this.error = '目录解析失败'
} finally {
this.loading = false
}
},
toggleTheme(isDark) {
if (isDark) {
document.body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme')
} else {
document.body.classList.remove('dark-theme')
document.documentElement.classList.remove('dark-theme')
}
}
},
mounted() {
this.fetchList()
}
}
</script>
<style scoped>
.show-list-page {
max-width: 900px;
margin: 40px auto;
}
.list-title-wrap {
text-align: center;
margin-bottom: 18px;
}
.list-title {
font-size: 2rem;
font-weight: bold;
color: #409eff;
margin-bottom: 4px;
word-break: break-all;
}
.list-subtitle {
font-size: 1.05rem;
color: #888;
margin-bottom: 2px;
}
.list-subtitle a {
color: #409eff;
text-decoration: underline;
}
.list-subtitle a:hover {
color: #1867c0;
}
</style>

View File

@@ -16,15 +16,27 @@ module.exports = {
host: '127.0.0.1',
port: 6444,
proxy: {
'/': {
target: 'http://127.0.0.1:6400', // 请求本地
'/parser': {
target: 'http://127.0.0.1:6400/', // 请求本地
ws: false
},
'/v2': {
target: 'http://127.0.0.1:6400/', // 请求本地
ws: false
},
'/json': {
target: 'http://127.0.0.1:6400/', // 请求本地
ws: false
},
'/d': {
target: 'http://127.0.0.1:6400/', // 请求本地
ws: false
},
}
},
configureWebpack: {
// provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
// it can be accessed in list.html to inject the correct title.
name: 'Netdisk fast download',
resolve: {
alias: {

View File

@@ -20,14 +20,15 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
/**
* 程序入口
* vertx程序入口
*
* <br>Create date 2021-05-08 13:00:01
*
* @author qaiu
* @author qaiu yyzy
*/
public class AppMain {
public static void main(String[] args) {
// start
Deploy.instance().start(args, AppMain::exec);
}

View File

@@ -3,29 +3,38 @@ package cn.qaiu.lz.common.cache;
import cn.qaiu.db.pool.JDBCPoolInit;
import cn.qaiu.db.pool.JDBCType;
import cn.qaiu.lz.web.model.CacheLinkInfo;
import cn.qaiu.lz.web.model.PanFileInfo;
import cn.qaiu.lz.web.model.PanFileInfoRowMapper;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import io.vertx.sqlclient.templates.SqlTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class CacheManager {
private final JDBCPool jdbcPool = JDBCPoolInit.instance().getPool();
private final Pool jdbcPool = JDBCPoolInit.instance().getPool();
private final JDBCType jdbcType = JDBCPoolInit.instance().getType();
private static final Logger LOGGER = LoggerFactory.getLogger(CacheManager.class);
public Future<CacheLinkInfo> get(String cacheKey) {
String sql = "SELECT share_key as shareKey, direct_link as directLink, expiration FROM cache_link_info WHERE share_key = #{share_key}";
String sql2 = "SELECT * FROM pan_file_info WHERE share_key = #{share_key}";
Map<String, Object> params = new HashMap<>();
params.put("share_key", cacheKey);
Promise<CacheLinkInfo> promise = Promise.promise();
Future<RowSet<PanFileInfo>> execute = SqlTemplate.forQuery(jdbcPool, sql2)
.mapTo(PanFileInfoRowMapper.INSTANCE)
.execute(params);
SqlTemplate.forQuery(jdbcPool, sql)
.mapTo(CacheLinkInfo.class)
.execute(params)
@@ -34,10 +43,18 @@ public class CacheManager {
if (rows.size() > 0) {
cacheHit = rows.iterator().next();
cacheHit.setCacheHit(true);
execute.onSuccess(r2 -> {
if (r2.size() > 0) {
cacheHit.setFileInfo(r2.iterator().next().toFileInfo());
}
promise.complete(cacheHit);
}).onFailure(e -> {
promise.complete(cacheHit);
});
} else {
cacheHit = new CacheLinkInfo(JsonObject.of("cacheHit", false, "shareKey", cacheKey));
promise.complete(cacheHit);
}
promise.complete(cacheHit);
}).onFailure(e->{
promise.fail(e);
LOGGER.error("cache get:", e);
@@ -47,7 +64,7 @@ public class CacheManager {
// 插入或更新缓存数据
public Future<Void> cacheShareLink(CacheLinkInfo cacheLinkInfo) {
public void cacheShareLink(CacheLinkInfo cacheLinkInfo) {
String sql;
if (jdbcType == JDBCType.MySQL) {
sql = """
@@ -63,12 +80,53 @@ public class CacheManager {
"VALUES (#{shareKey}, #{directLink}, #{expiration})";
}
// 直接传递 CacheLinkInfo 实体类
return SqlTemplate.forUpdate(jdbcPool, sql)
SqlTemplate.forUpdate(jdbcPool, sql)
.mapFrom(CacheLinkInfo.class) // 将实体类映射为 Tuple 参数
.execute(cacheLinkInfo)
.mapEmpty();
.execute(cacheLinkInfo).onSuccess(result -> {
if (result.rowCount() > 0) {
LOGGER.debug("Cache link info updated for shareKey: {}", cacheLinkInfo.getShareKey());
} else {
LOGGER.warn("No rows affected when updating cache link info for shareKey: {}", cacheLinkInfo.getShareKey());
}
}).onFailure(Throwable::printStackTrace);
if (cacheLinkInfo.getFileInfo() != null) {
String sql2 = """
INSERT IGNORE INTO pan_file_info (
share_key, file_name, file_id, file_icon, size, size_str, file_type,
file_path, create_time, update_time, create_by, description, download_count,
pan_type, parser_url, preview_url, hash
) VALUES (
#{shareKey}, #{fileName}, #{fileId}, #{fileIcon}, #{size}, #{sizeStr}, #{fileType},
#{filePath}, #{createTime}, #{updateTime}, #{createBy}, #{description}, #{downloadCount},
#{panType}, #{parserUrl}, #{previewUrl}, #{hash}
);
""";
// 判断文件信息是否缓存
SqlTemplate
.forQuery(jdbcPool, "SELECT count(1) AS count FROM pan_file_info WHERE share_key = #{share_key};")
.mapTo(Row::toJson)
.execute(Collections.singletonMap("share_key", cacheLinkInfo.getShareKey()))
.onSuccess(rows -> {
JsonObject row = rows.iterator().next();
int count = row.getInteger("count");
if (count == 0) {
// 没有缓存,执行插入
PanFileInfo fileInfo = PanFileInfo.fromFileInfo(cacheLinkInfo.getFileInfo());
fileInfo.setShareKey(cacheLinkInfo.getShareKey());
SqlTemplate.forUpdate(jdbcPool, sql2)
.mapFrom(PanFileInfo.class) // 将实体类映射为 Tuple 参数
.execute(fileInfo).onSuccess(result -> {
if (result.rowCount() > 0) {
LOGGER.debug("Pan file info inserted for shareKey: {}", cacheLinkInfo.getShareKey());
} else {
LOGGER.warn("No rows affected when inserting pan file info for shareKey: {}", cacheLinkInfo.getShareKey());
}
}).onFailure(Throwable::printStackTrace);
}
});
}
}
// 写入网盘厂商API解析次数

View File

@@ -0,0 +1,185 @@
package cn.qaiu.lz.common.util;
import cn.qaiu.lz.web.model.SysUser;
import io.vertx.core.json.JsonObject;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Date;
/**
* JWT工具类用于生成和验证JWT token
*/
public class JwtUtil {
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000; // token过期时间24小时
private static final String SECRET_KEY = "netdisk-fast-download-jwt-secret-key"; // 密钥
private static final String ALGORITHM = "HmacSHA256";
/**
* 生成JWT token
*
* @param user 用户信息
* @return JWT token
*/
public static String generateToken(SysUser user) {
long expireTime = getExpireTime();
// Header
JsonObject header = new JsonObject()
.put("alg", "HS256")
.put("typ", "JWT");
// Payload
JsonObject payload = new JsonObject()
.put("id", user.getId())
.put("username", user.getUsername())
.put("role", user.getRole())
.put("exp", expireTime)
.put("iat", System.currentTimeMillis())
.put("iss", "netdisk-fast-download");
// Base64 encode header and payload
String encodedHeader = Base64.getUrlEncoder().withoutPadding().encodeToString(header.encode().getBytes(StandardCharsets.UTF_8));
String encodedPayload = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.encode().getBytes(StandardCharsets.UTF_8));
// Create signature
String signature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
// Combine to form JWT
return encodedHeader + "." + encodedPayload + "." + signature;
}
/**
* 使用HMAC-SHA256算法生成签名
*
* @param data 要签名的数据
* @param key 密钥
* @return 签名
*/
private static String hmacSha256(String data, String key) {
try {
Mac sha256Hmac = Mac.getInstance(ALGORITHM);
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
sha256Hmac.init(secretKey);
byte[] signedBytes = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().withoutPadding().encodeToString(signedBytes);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Error creating HMAC SHA256 signature", e);
}
}
/**
* 验证JWT token
*
* @param token JWT token
* @return 如果token有效返回true否则返回false
*/
public static boolean validateToken(String token) {
try {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return false;
}
String encodedHeader = parts[0];
String encodedPayload = parts[1];
String signature = parts[2];
// 验证签名
String expectedSignature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
if (!expectedSignature.equals(signature)) {
return false;
}
// 验证过期时间
String payload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
JsonObject payloadJson = new JsonObject(payload);
long expTime = payloadJson.getLong("exp", 0L);
return System.currentTimeMillis() < expTime;
} catch (Exception e) {
return false;
}
}
/**
* 从token中获取用户ID
*
* @param token JWT token
* @return 用户ID
*/
public static String getUserIdFromToken(String token) {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return null;
}
// Base64解码
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
JsonObject jsonObject = new JsonObject(payload);
return jsonObject.getString("id");
}
/**
* 从token中获取用户名
*
* @param token JWT token
* @return 用户名
*/
public static String getUsernameFromToken(String token) {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return null;
}
// Base64解码
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
JsonObject jsonObject = new JsonObject(payload);
return jsonObject.getString("username");
}
/**
* 从token中获取用户角色
*
* @param token JWT token
* @return 用户角色
*/
public static String getRoleFromToken(String token) {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return null;
}
// Base64解码
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
JsonObject jsonObject = new JsonObject(payload);
return jsonObject.getString("role");
}
/**
* 获取过期时间
*
* @return 过期时间戳
*/
private static long getExpireTime() {
return System.currentTimeMillis() + EXPIRE_TIME;
}
/**
* 将过期时间戳转换为LocalDateTime
*
* @param expireTime 过期时间戳
* @return LocalDateTime
*/
public static LocalDateTime getExpireTimeAsLocalDateTime(long expireTime) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(expireTime), ZoneId.systemDefault());
}
}

View File

@@ -0,0 +1,90 @@
package cn.qaiu.lz.common.util;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 密码加密工具类
* 使用SHA-256算法加盐进行密码加密和验证
*/
public class PasswordUtil {
private static final String ALGORITHM = "SHA-256";
private static final int SALT_LENGTH = 16; // 盐的长度
private static final String DELIMITER = ":"; // 用于分隔盐和哈希值的分隔符
/**
* 对密码进行加密
*
* @param plainPassword 明文密码
* @return 加密后的密码格式salt:hash
*/
public static String hashPassword(String plainPassword) {
if (plainPassword == null || plainPassword.isEmpty()) {
throw new IllegalArgumentException("密码不能为空");
}
try {
// 生成随机盐
SecureRandom random = new SecureRandom();
byte[] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
// 计算哈希值
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
md.update(salt);
byte[] hashedPassword = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
// 将盐和哈希值编码为Base64并拼接
String saltBase64 = Base64.getEncoder().encodeToString(salt);
String hashBase64 = Base64.getEncoder().encodeToString(hashedPassword);
// 返回格式salt:hash
return saltBase64 + DELIMITER + hashBase64;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("加密算法不可用", e);
}
}
/**
* 验证密码是否正确
*
* @param plainPassword 明文密码
* @param hashedPassword 加密后的密码格式salt:hash
* @return 如果密码匹配返回true否则返回false
*/
public static boolean checkPassword(String plainPassword, String hashedPassword) {
if (plainPassword == null || hashedPassword == null || hashedPassword.isEmpty()) {
return false;
}
try {
// 分割盐和哈希值
String[] parts = hashedPassword.split(DELIMITER);
if (parts.length != 2) {
return false;
}
String saltBase64 = parts[0];
String hashBase64 = parts[1];
// 解码盐
byte[] salt = Base64.getDecoder().decode(saltBase64);
// 使用相同的盐计算哈希值
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
md.update(salt);
byte[] calculatedHash = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
String calculatedHashBase64 = Base64.getEncoder().encodeToString(calculatedHash);
// 比较计算出的哈希值和存储的哈希值
return hashBase64.equals(calculatedHashBase64);
} catch (Exception e) {
// 如果发生异常例如格式不正确返回false
return false;
}
}
}

View File

@@ -1,7 +1,13 @@
package cn.qaiu.lz.common.util;
import cn.qaiu.parser.ParserCreate;
import cn.qaiu.vx.core.util.ConfigConstant;
import cn.qaiu.vx.core.util.SharedDataUtil;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@@ -59,7 +65,7 @@ public class URLParamUtil {
boolean firstParam = !decodedUrl.contains("?");
for (String paramName : params.names()) {
if (!paramName.equals("url") && !paramName.equals("pwd")) { // 忽略 "url" 和 "pwd" 参数
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId") && !paramName.equals("uuid")) { // 忽略 "url" 和 "pwd" 参数
if (firstParam) {
urlBuilder.append("?");
firstParam = false;
@@ -72,4 +78,42 @@ public class URLParamUtil {
return urlBuilder.toString();
}
/**
* 添加共享链接的其他参数到ParserCreate对象中
* @param parserCreate ParserCreate对象包含共享链接信息
*/
public static void addParam(ParserCreate parserCreate) {
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData()
.getLocalMap(ConfigConstant.LOCAL);
String type = parserCreate.getShareLinkInfo().getType();
if (localMap.containsKey(ConfigConstant.PROXY)) {
JsonObject proxy = (JsonObject) localMap.get(ConfigConstant.PROXY);
if (proxy.containsKey(type)) {
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.PROXY, proxy.getJsonObject(type));
}
}
if (localMap.containsKey(ConfigConstant.AUTHS)) {
JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS);
if (auths.containsKey(type)) {
// 需要处理引号
MultiMap entries = MultiMap.caseInsensitiveMultiMap();
JsonObject jsonObject = auths.getJsonObject(type);
if (jsonObject != null) {
jsonObject.forEach(entity -> {
if (entity == null || entity.getValue() == null) {
return;
}
entries.set(entity.getKey(), entity.getValue().toString());
});
}
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.AUTHS, entries);
}
}
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
}
}

View File

@@ -5,6 +5,7 @@ import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.common.cache.CacheManager;
import cn.qaiu.lz.common.util.URLParamUtil;
import cn.qaiu.lz.web.model.CacheLinkInfo;
import cn.qaiu.lz.web.model.LinkInfoResp;
import cn.qaiu.lz.web.model.StatisticsInfo;
import cn.qaiu.lz.web.model.SysUser;
@@ -15,6 +16,7 @@ import cn.qaiu.parser.ParserCreate;
import cn.qaiu.vx.core.annotaions.RouteHandler;
import cn.qaiu.vx.core.annotaions.RouteMapping;
import cn.qaiu.vx.core.enums.RouteMethod;
import cn.qaiu.vx.core.model.JsonResult;
import cn.qaiu.vx.core.util.AsyncServiceUtil;
import cn.qaiu.vx.core.util.ResponseUtil;
import cn.qaiu.vx.core.util.SharedDataUtil;
@@ -26,6 +28,8 @@ import io.vertx.core.json.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
@@ -33,14 +37,8 @@ import java.util.stream.Collectors;
@Slf4j
public class ParserApi {
private final UserService userService = AsyncServiceUtil.getAsyncServiceInstance(UserService.class);
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
@RouteMapping(value = "/login", method = RouteMethod.GET)
public Future<SysUser> login(SysUser user) {
log.info("<------- login: {}", user.getUsername());
return userService.login(user);
}
@RouteMapping(value = "/statisticsInfo", method = RouteMethod.GET, order = 99)
public Future<StatisticsInfo> statisticsInfo() {
@@ -48,6 +46,7 @@ public class ParserApi {
}
private final CacheManager cacheManager = new CacheManager();
private final ServerApi serverApi = new ServerApi();
@RouteMapping(value = "/linkInfo", method = RouteMethod.GET)
public Future<LinkInfoResp> parse(HttpServerRequest request, String pwd) {
@@ -58,6 +57,7 @@ public class ParserApi {
LinkInfoResp build = LinkInfoResp.builder()
.downLink(getDownLink(parserCreate, false))
.apiLink(getDownLink(parserCreate, true))
.viewLink(getViewLink(parserCreate))
.shareLinkInfo(shareLinkInfo).build();
// 解析次数统计
shareLinkInfo.getOtherParam().put("UA",request.headers().get("user-agent"));
@@ -85,6 +85,15 @@ public class ParserApi {
return linkPrefix + (isJson ? "/json/" : "/d/") + create.genPathSuffix();
}
private static String getViewLink(ParserCreate create) {
String linkPrefix = SharedDataUtil.getJsonStringForServerConfig("domainName");
if (StringUtils.isBlank(linkPrefix)) {
return "";
}
return linkPrefix + "/v2/view/" + create.genPathSuffix();
}
/**
* 获取支持的网盘列表
* @return list-map: name: 网盘名, type: 网盘标识, url: 网盘域名地址
@@ -100,11 +109,17 @@ public class ParserApi {
}
@RouteMapping("/getFileList")
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd) {
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) {
String url = URLParamUtil.parserParams(request);
ParserCreate parserCreate = ParserCreate.fromShareUrl(url).setShareLinkInfoPwd(pwd);
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
if (StringUtils.isNotBlank(dirId)) {
parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId);
}
if (StringUtils.isNotBlank(uuid)) {
parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid);
}
return parserCreate.createTool().parseFileList();
}
@@ -130,7 +145,6 @@ public class ParserApi {
return parserCreate.createTool().parseById();
}
@RouteMapping("/redirectUrl/:type/:param")
public Future<Void> redirectUrl(HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise();
@@ -140,4 +154,51 @@ public class ParserApi {
.onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future();
}
/**
* 预览媒体文件
*/
@RouteMapping(value = "/view/:type/:key", method = RouteMethod.GET, order = 2)
public void view(HttpServerRequest request, HttpServerResponse response, String type, String key) {
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
serverApi.parseKeyJson(request, type, key).onSuccess(res -> {
redirect(response, previewURL, res);
}).onFailure(e -> {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
});
}
private static void redirect(HttpServerResponse response, String previewURL, CacheLinkInfo res) {
String directLink = res.getDirectLink();
ResponseUtil.redirect(response, previewURL + URLEncoder.encode(directLink, StandardCharsets.UTF_8));
}
/**
* 预览媒体文件-目录预览
*/
@RouteMapping(value = "/preview", method = RouteMethod.GET, order = 9)
public void viewURL(HttpServerRequest request, HttpServerResponse response, String pwd) {
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
new ServerApi().parseJson(request, pwd).onSuccess(res -> {
redirect(response, previewURL, res);
}).onFailure(e -> {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
});
}
@RouteMapping("/viewUrl/:type/:param")
public Future<Void> viewUrl(HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise();
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
getFileDownUrl(type, param)
.onSuccess(res -> {
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
ResponseUtil.redirect(response, url);
})
.onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future();
}
}

View File

@@ -5,8 +5,10 @@ import cn.qaiu.db.ddl.Table;
import cn.qaiu.db.ddl.TableGenIgnore;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.lz.common.ToJson;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.vertx.codegen.annotations.DataObject;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.jackson.DatabindCodec;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -66,6 +68,11 @@ public class CacheLinkInfo implements ToJson {
if (json.containsKey("expiration")) {
this.setExpiration(json.getLong("expiration"));
}
if (json.containsKey("fileInfo")) {
ObjectMapper mapper = DatabindCodec.mapper(); // Vert.x 自带的 mapper
this.setFileInfo(mapper.convertValue(json.getJsonObject("fileInfo"), FileInfo.class));
}
this.setCacheHit(json.getBoolean("cacheHit", false));
}

View File

@@ -0,0 +1,163 @@
package cn.qaiu.lz.web.model;
import cn.qaiu.db.ddl.Table;
import cn.qaiu.entity.FileInfo;
import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.format.SnakeCase;
import io.vertx.core.json.JsonObject;
import io.vertx.sqlclient.templates.annotations.RowMapped;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author <a href="https://qaiu.top">QAIU</a>
* @date 2025/8/4 12:38
*/
@Table(keyFields = "share_key")
@DataObject
@RowMapped(formatter = SnakeCase.class)
@NoArgsConstructor
@Data
public class PanFileInfo {
String shareKey;
/**
* 文件名
*/
private String fileName;
/**
* 文件ID
*/
private String fileId;
private String fileIcon;
/**
* 文件大小(byte)
*/
private Long size;
private String sizeStr;
/**
* 类型
*/
private String fileType;
/**
* 文件路径
*/
private String filePath;
/**
* 创建(上传)时间 yyyy-MM-dd HH:mm:ss格式
*/
private String createTime;
/**
* 上次修改时间
*/
private String updateTime;
/**
* 创建者
*/
private String createBy;
/**
* 文件描述
*/
private String description;
/**
* 下载次数
*/
private Integer downloadCount;
/**
* 网盘标识
*/
private String panType;
/**
* nfd下载链接(可能获取不到)
* note: 不是下载直链
*/
private String parserUrl;
//预览地址
private String previewUrl;
// 文件hash默认类型为md5
private String hash;
public PanFileInfo(JsonObject jsonObject) {
this.shareKey = jsonObject.getString("shareKey");
this.fileName = jsonObject.getString("fileName");
this.fileId = jsonObject.getString("fileId");
this.fileIcon = jsonObject.getString("fileIcon");
this.size = jsonObject.getLong("size");
this.sizeStr = jsonObject.getString("sizeStr");
this.fileType = jsonObject.getString("fileType");
this.filePath = jsonObject.getString("filePath");
this.createTime = jsonObject.getString("createTime");
this.updateTime = jsonObject.getString("updateTime");
this.createBy = jsonObject.getString("createBy");
this.description = jsonObject.getString("description");
this.downloadCount = jsonObject.getInteger("downloadCount");
this.panType = jsonObject.getString("panType");
this.parserUrl = jsonObject.getString("parserUrl");
this.previewUrl = jsonObject.getString("previewUrl");
this.hash = jsonObject.getString("hash");
}
public static PanFileInfo fromFileInfo(FileInfo info) {
PanFileInfo panFileInfo = new PanFileInfo();
if (info == null) {
return panFileInfo;
}
// 拷贝 FileInfo 的字段
panFileInfo.setFileName(info.getFileName());
panFileInfo.setFileId(info.getFileId());
panFileInfo.setFileIcon(info.getFileIcon());
panFileInfo.setSize(info.getSize());
panFileInfo.setSizeStr(info.getSizeStr());
panFileInfo.setFileType(info.getFileType());
panFileInfo.setFilePath(info.getFilePath());
panFileInfo.setCreateTime(info.getCreateTime());
panFileInfo.setUpdateTime(info.getUpdateTime());
panFileInfo.setCreateBy(info.getCreateBy());
panFileInfo.setDescription(info.getDescription());
panFileInfo.setDownloadCount(info.getDownloadCount());
panFileInfo.setPanType(info.getPanType());
panFileInfo.setParserUrl(info.getParserUrl());
panFileInfo.setPreviewUrl(info.getPreviewUrl());
panFileInfo.setHash(info.getHash());
return panFileInfo;
}
public FileInfo toFileInfo() {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(this.getFileName());
fileInfo.setFileId(this.getFileId());
fileInfo.setFileIcon(this.getFileIcon());
fileInfo.setSize(this.getSize());
fileInfo.setSizeStr(this.getSizeStr());
fileInfo.setFileType(this.getFileType());
fileInfo.setFilePath(this.getFilePath());
fileInfo.setCreateTime(this.getCreateTime());
fileInfo.setUpdateTime(this.getUpdateTime());
fileInfo.setCreateBy(this.getCreateBy());
fileInfo.setDescription(this.getDescription());
fileInfo.setDownloadCount(this.getDownloadCount());
fileInfo.setPanType(this.getPanType());
fileInfo.setParserUrl(this.getParserUrl());
fileInfo.setPreviewUrl(this.getPreviewUrl());
fileInfo.setHash(this.getHash());
return fileInfo;
}
}

View File

@@ -3,6 +3,7 @@ package cn.qaiu.lz.web.model;
import cn.qaiu.db.ddl.Table;
import cn.qaiu.lz.common.ToJson;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.vertx.codegen.annotations.DataObject;
import io.vertx.core.json.JsonObject;
import lombok.Data;
@@ -14,12 +15,27 @@ import java.time.format.DateTimeFormatter;
@Data
@DataObject
@NoArgsConstructor
@Table("t_user")
@Table("sys_user")
public class SysUser implements ToJson {
private String id;
private String username;
private String password;
private String email;
private String phone;
private String avatar;
// 用户状态0-禁用1-正常
private Integer status;
// 用户角色admin-管理员user-普通用户
private String role;
// 最后登录时间
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime lastLoginTime;
private Integer age;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createTime;
@@ -28,9 +44,17 @@ public class SysUser implements ToJson {
this.id = json.getString("id");
this.username = json.getString("username");
this.password = json.getString("password");
this.email = json.getString("email");
this.phone = json.getString("phone");
this.avatar = json.getString("avatar");
this.status = json.getInteger("status");
this.role = json.getString("role");
this.age = json.getInteger("age");
if (json.getString("createTime") != null) {
this.createTime = LocalDateTime.parse(json.getString("createTime"));
}
if (json.getString("lastLoginTime") != null) {
this.lastLoginTime = LocalDateTime.parse(json.getString("lastLoginTime"));
}
}
}

View File

@@ -4,14 +4,48 @@ import cn.qaiu.lz.web.model.SysUser;
import cn.qaiu.vx.core.base.BaseAsyncService;
import io.vertx.codegen.annotations.ProxyGen;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
/**
* lz-web
* 用户服务接口
* <br>Create date 2021/8/27 14:06
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
@ProxyGen
public interface UserService extends BaseAsyncService {
Future<SysUser> login(SysUser user);
/**
* 用户登录
* @param user 包含用户名和密码的用户对象
* @return 登录成功返回用户信息和token失败返回错误信息
*/
Future<JsonObject> login(SysUser user);
/**
* 根据用户名获取用户信息
* @param username 用户名
* @return 用户信息
*/
Future<SysUser> getUserByUsername(String username);
/**
* 创建新用户
* @param user 用户信息
* @return 创建成功返回用户信息,失败返回错误信息
*/
Future<SysUser> createUser(SysUser user);
/**
* 更新用户信息
* @param user 用户信息
* @return 更新成功返回用户信息,失败返回错误信息
*/
Future<SysUser> updateUser(SysUser user);
/**
* 验证token
* @param token JWT token
* @return 验证成功返回用户信息,失败返回错误信息
*/
Future<JsonObject> validateToken(String token);
}

View File

@@ -1,38 +1,33 @@
package cn.qaiu.lz.web.service.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.common.cache.CacheConfigLoader;
import cn.qaiu.lz.common.cache.CacheManager;
import cn.qaiu.lz.common.cache.CacheTotalField;
import cn.qaiu.lz.common.util.URLParamUtil;
import cn.qaiu.lz.web.model.CacheLinkInfo;
import cn.qaiu.lz.web.service.CacheService;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.ParserCreate;
import cn.qaiu.vx.core.annotaions.Service;
import cn.qaiu.vx.core.util.ConfigConstant;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.util.Date;
@Service
@Slf4j
public class CacheServiceImpl implements CacheService {
private final CacheManager cacheManager = new CacheManager();
private Future<CacheLinkInfo> getAndSaveCachedShareLink(ParserCreate parserCreate) {
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData()
.getLocalMap(ConfigConstant.LOCAL);
if (localMap.containsKey(ConfigConstant.PROXY)) {
JsonObject proxy = (JsonObject) localMap.get(ConfigConstant.PROXY);
String type = parserCreate.getShareLinkInfo().getType();
if (proxy.containsKey(type)) {
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.PROXY, proxy.getJsonObject(type));
}
}
URLParamUtil.addParam(parserCreate);
Promise<CacheLinkInfo> promise = Promise.promise();
// 构建组合的缓存key
@@ -46,20 +41,36 @@ public class CacheServiceImpl implements CacheService {
// parse
result.setCacheHit(false);
result.setExpiration(0L);
parserCreate.createTool().parse().onSuccess(redirectUrl -> {
IPanTool tool;
try {
tool = parserCreate.createTool();
} catch (Exception e) {
promise.fail(e.getCause().getCause());
return;
}
tool.parse().onSuccess(redirectUrl -> {
long expires = System.currentTimeMillis() +
CacheConfigLoader.getDuration(shareLinkInfo.getType()) * 60 * 1000L;
result.setDirectLink(redirectUrl);
// result.setExpires(generateDate(expires));
promise.complete(result);
// 更新缓存
// 将直链存储到缓存
CacheLinkInfo cacheLinkInfo = new CacheLinkInfo(JsonObject.of(
"directLink", redirectUrl,
"expiration", expires,
"shareKey", cacheKey
));
cacheManager.cacheShareLink(cacheLinkInfo).onFailure(Throwable::printStackTrace);
if (shareLinkInfo.getOtherParam().containsKey("fileInfo")) {
try {
FileInfo fileInfo = (FileInfo) shareLinkInfo.getOtherParam().get("fileInfo");
result.setFileInfo(fileInfo);
cacheLinkInfo.setFileInfo(fileInfo);
} catch (Exception ignored) {
log.error("文件对象转换异常");
}
}
promise.complete(result);
// 更新缓存
// 将直链存储到缓存
cacheManager.cacheShareLink(cacheLinkInfo);
cacheManager.updateTotalByField(cacheKey, CacheTotalField.API_PARSER_TOTAL).onFailure(Throwable::printStackTrace);
}).onFailure(promise::fail);
} else {

View File

@@ -1,29 +1,414 @@
package cn.qaiu.lz.web.service.impl;
import cn.qaiu.db.pool.JDBCPoolInit;
import cn.qaiu.lz.common.util.JwtUtil;
import cn.qaiu.lz.common.util.PasswordUtil;
import cn.qaiu.lz.web.model.SysUser;
import cn.qaiu.lz.web.service.UserService;
import cn.qaiu.vx.core.annotaions.Service;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import io.vertx.sqlclient.Tuple;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.UUID;
/**
* lz-web
* 用户服务实现类
* <br>Create date 2021/8/27 14:09
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Override
public Future<SysUser> login(SysUser user) {
private final JDBCPool jdbcPool = JDBCPoolInit.instance().getPool();
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
return Future.succeededFuture(user);
// 初始化方法,确保管理员用户存在
public void init() {
// 检查管理员用户是否存在
getUserByUsername("admin")
.onSuccess(user -> {
log.info("管理员用户已存在");
})
.onFailure(err -> {
// 创建管理员用户
SysUser admin = new SysUser();
admin.setId(UUID.randomUUID().toString());
admin.setUsername("admin");
admin.setPassword(PasswordUtil.hashPassword("admin123"));
admin.setEmail("admin@example.com");
admin.setRole("admin");
admin.setStatus(1);
admin.setCreateTime(LocalDateTime.now());
createUser(admin)
.onSuccess(result -> log.info("管理员用户创建成功"))
.onFailure(error -> log.error("管理员用户创建失败", error));
});
}
}
// 新增一个工具方法来过滤敏感信息
private SysUser filterSensitiveInfo(SysUser user) {
if (user != null) {
SysUser filtered = new SysUser();
// 复制除密码外的所有字段
filtered.setId(user.getId());
filtered.setUsername(user.getUsername());
filtered.setEmail(user.getEmail());
filtered.setPhone(user.getPhone());
filtered.setAvatar(user.getAvatar());
filtered.setRole(user.getRole());
filtered.setStatus(user.getStatus());
filtered.setCreateTime(user.getCreateTime());
filtered.setLastLoginTime(user.getLastLoginTime());
return filtered;
}
return null;
}
// 将Row转换为SysUser对象
private SysUser rowToUser(Row row) {
if (row == null) {
return null;
}
SysUser user = new SysUser();
user.setId(row.getString("id"));
user.setUsername(row.getString("username"));
user.setPassword(row.getString("password"));
user.setEmail(row.getString("email"));
user.setPhone(row.getString("phone"));
user.setAvatar(row.getString("avatar"));
user.setRole(row.getString("role"));
user.setStatus(row.getInteger("status"));
// 处理日期时间字段
LocalDateTime createTime = row.getLocalDateTime("create_time");
if (createTime != null) {
user.setCreateTime(createTime);
}
LocalDateTime lastLoginTime = row.getLocalDateTime("last_login_time");
if (lastLoginTime != null) {
user.setLastLoginTime(lastLoginTime);
}
return user;
}
@Override
public Future<JsonObject> login(SysUser user) {
// 参数校验
if (user == null || user.getUsername() == null || user.getPassword() == null) {
return Future.succeededFuture(new JsonObject()
.put("success", false)
.put("message", "用户名和密码不能为空"));
}
Promise<JsonObject> promise = Promise.promise();
// 查询用户
String sql = "SELECT * FROM sys_user WHERE username = ?";
jdbcPool.preparedQuery(sql)
.execute(Tuple.of(user.getUsername()))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "用户不存在"));
return;
}
Row row = rows.iterator().next();
SysUser existUser = rowToUser(row);
// 验证密码
if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "密码错误"));
return;
}
// 更新最后登录时间
LocalDateTime now = LocalDateTime.now();
existUser.setLastLoginTime(now);
// 更新数据库中的最后登录时间
String updateSql = "UPDATE sys_user SET last_login_time = ? WHERE username = ?";
jdbcPool.preparedQuery(updateSql)
.execute(Tuple.of(
Timestamp.from(now.atZone(ZoneId.systemDefault()).toInstant()),
existUser.getUsername()
))
.onFailure(err -> log.error("更新最后登录时间失败", err));
// 生成token
String token = JwtUtil.generateToken(existUser);
// 返回用户信息和token
JsonObject value = JsonObject.mapFrom(existUser);
value.remove("password");
promise.complete(new JsonObject()
.put("success", true)
.put("message", "登录成功")
.put("token", token)
.put("user", value));
})
.onFailure(err -> {
log.error("登录查询失败", err);
promise.complete(new JsonObject()
.put("success", false)
.put("message", "登录失败: " + err.getMessage()));
});
return promise.future();
}
@Override
public Future<SysUser> getUserByUsername(String username) {
if (username == null || username.isEmpty()) {
return Future.failedFuture("用户名不能为空");
}
Promise<SysUser> promise = Promise.promise();
String sql = "SELECT * FROM sys_user WHERE username = ?";
jdbcPool.preparedQuery(sql)
.execute(Tuple.of(username))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.fail("用户不存在");
return;
}
Row row = rows.iterator().next();
SysUser user = rowToUser(row);
promise.complete(filterSensitiveInfo(user));
})
.onFailure(err -> {
log.error("查询用户失败", err);
promise.fail("查询用户失败: " + err.getMessage());
});
return promise.future();
}
@Override
public Future<SysUser> createUser(SysUser user) {
// 参数校验
if (user == null || user.getUsername() == null || user.getPassword() == null) {
return Future.failedFuture("用户名和密码不能为空");
}
Promise<SysUser> promise = Promise.promise();
// 先检查用户是否已存在
String checkSql = "SELECT COUNT(*) as count FROM sys_user WHERE username = ?";
jdbcPool.preparedQuery(checkSql)
.execute(Tuple.of(user.getUsername()))
.onSuccess(rows -> {
Row row = rows.iterator().next();
long count = row.getLong("count");
if (count > 0) {
promise.fail("用户名已存在");
return;
}
// 设置用户ID和创建时间
if (user.getId() == null) {
user.setId(UUID.randomUUID().toString());
}
if (user.getCreateTime() == null) {
user.setCreateTime(LocalDateTime.now());
}
// 设置默认角色和状态
if (user.getRole() == null) {
user.setRole("user");
}
if (user.getStatus() == null) {
user.setStatus(1);
}
// 对密码进行加密
String plainPassword = user.getPassword();
user.setPassword(PasswordUtil.hashPassword(plainPassword));
// 插入用户
String insertSql = "INSERT INTO sys_user (id, username, password, email, phone, avatar, role, status, create_time) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
jdbcPool.preparedQuery(insertSql)
.execute(Tuple.of(
user.getId(),
user.getUsername(),
user.getPassword(),
user.getEmail(),
user.getPhone(),
user.getAvatar(),
user.getRole(),
user.getStatus(),
Timestamp.from(user.getCreateTime().atZone(ZoneId.systemDefault()).toInstant())
))
.onSuccess(result -> {
promise.complete(filterSensitiveInfo(user));
})
.onFailure(err -> {
log.error("创建用户失败", err);
promise.fail("创建用户失败: " + err.getMessage());
});
})
.onFailure(err -> {
log.error("检查用户是否存在失败", err);
promise.fail("创建用户失败: " + err.getMessage());
});
return promise.future();
}
@Override
public Future<SysUser> updateUser(SysUser user) {
// 参数校验
if (user == null || user.getUsername() == null) {
return Future.failedFuture("用户名不能为空");
}
Promise<SysUser> promise = Promise.promise();
// 先检查用户是否存在
String checkSql = "SELECT * FROM sys_user WHERE username = ?";
jdbcPool.preparedQuery(checkSql)
.execute(Tuple.of(user.getUsername()))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.fail("用户不存在");
return;
}
Row row = rows.iterator().next();
SysUser existUser = rowToUser(row);
// 构建更新SQL
StringBuilder updateSql = new StringBuilder("UPDATE sys_user SET ");
Tuple params = Tuple.tuple();
if (user.getEmail() != null) {
updateSql.append("email = ?, ");
params.addValue(user.getEmail());
}
if (user.getPhone() != null) {
updateSql.append("phone = ?, ");
params.addValue(user.getPhone());
}
if (user.getAvatar() != null) {
updateSql.append("avatar = ?, ");
params.addValue(user.getAvatar());
}
if (user.getStatus() != null) {
updateSql.append("status = ?, ");
params.addValue(user.getStatus());
}
if (user.getRole() != null) {
updateSql.append("role = ?, ");
params.addValue(user.getRole());
}
if (user.getPassword() != null) {
updateSql.append("password = ?, ");
params.addValue(PasswordUtil.hashPassword(user.getPassword()));
}
// 移除最后的逗号和空格
String sql = updateSql.toString();
if (sql.endsWith(", ")) {
sql = sql.substring(0, sql.length() - 2);
}
// 如果没有要更新的字段,直接返回
if (params.size() == 0) {
promise.complete(filterSensitiveInfo(existUser));
return;
}
// 添加WHERE条件
sql += " WHERE username = ?";
params.addValue(user.getUsername());
// 执行更新
jdbcPool.preparedQuery(sql)
.execute(params)
.onSuccess(result -> {
// 重新查询用户信息
getUserByUsername(user.getUsername())
.onSuccess(promise::complete)
.onFailure(promise::fail);
})
.onFailure(err -> {
log.error("更新用户失败", err);
promise.fail("更新用户失败: " + err.getMessage());
});
})
.onFailure(err -> {
log.error("查询用户失败", err);
promise.fail("更新用户失败: " + err.getMessage());
});
return promise.future();
}
@Override
public Future<JsonObject> validateToken(String token) {
if (token == null || token.isEmpty()) {
return Future.succeededFuture(new JsonObject()
.put("success", false)
.put("message", "Token不能为空"));
}
// 验证token
boolean isValid = JwtUtil.validateToken(token);
if (!isValid) {
return Future.succeededFuture(new JsonObject()
.put("success", false)
.put("message", "Token无效或已过期"));
}
// 获取用户信息
String username = JwtUtil.getUsernameFromToken(token);
Promise<JsonObject> promise = Promise.promise();
getUserByUsername(username)
.onSuccess(user -> {
promise.complete(new JsonObject()
.put("success", true)
.put("message", "Token有效")
.put("user", JsonObject.mapFrom(user)));
})
.onFailure(err -> {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "用户不存在"));
});
return promise.future();
}
}

View File

@@ -6,6 +6,8 @@ server:
enableDatabase: true
# 服务域名或者IP 生成二维码链接时需要
domainName: http://127.0.0.1:6401
# 预览服务URL
previewURL: https://nfd-parser.github.io/nfd-preview/preview.html?src=
# domainName: https://lz.qaiu.top
# 反向代理服务器配置路径(不用加后缀)
@@ -34,15 +36,15 @@ custom:
- ^cn\.qaiu\.lz\.web\.model\..*
# 限流配置
rateLimit:
# 是否启用限流
enable: true
# 限流的请求数
limit: 5
# 限流的时间窗口(单位秒)
timeWindow: 10
# 路径匹配规则
pathReg: ^/v2/.*
#rateLimit:
# # 是否启用限流
# enable: true
# # 限流的请求数
# limit: 10
# # 限流的时间窗口(单位秒)
# timeWindow: 10
# # 路径匹配规则
# pathReg: ^/v2/.*
# 数据源配置
@@ -61,7 +63,7 @@ cache:
# 该配置未使用后续加入其他Cache实现时区分类型
type: h2db
# 默认时长: 单位分钟,大部分网盘未严格验证,建议不要太大
defaultDuration: 59
defaultDuration: 5
# 具体网盘的缓存配置如果不加配置则不缓存每次请求都会请求网盘API格式网盘标识: 时长
duration:
ce: 5
@@ -91,7 +93,3 @@ proxy:
# username:
# password:
# 代理池配置
#ip-pool:
# api-url:

View File

@@ -0,0 +1,90 @@
# 网盘分享链接云解析服务 API 测试
# 本文件包含了系统所有API接口的测试请求
# 使用方法:
# 1. 先运行登录接口获取token
# 2. 将返回的token替换所有请求中的YOUR_TOKEN_HERE
# 3. 对于需要ID的请求将实际ID替换TOKEN_ID
### 用户接口 ###
### 登录接口
POST http://localhost:6400/api/user/login
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}
### 用户注册
POST http://localhost:6400/api/user/register
Content-Type: application/json
{
"username": "testuser",
"password": "password123",
"email": "testuser@example.com",
"phone": "13800138000"
}
### 获取用户信息
# 使用登录接口返回的token替换下面的YOUR_TOKEN_HERE
GET http://localhost:6400/api/user/info
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
### 验证Token
POST http://localhost:6400/api/user/validate-token
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc"
}
### 更新用户信息
PUT http://localhost:6400/api/user/update
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
{
"email": "new-email@example.com",
"phone": "13900139000",
"avatar": "https://example.com/avatar.jpg"
}
### 管理员接口 ###
### 获取所有网盘Token
GET http://localhost:6400/api/admin/tokens
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
### 添加网盘Token
POST http://localhost:6400/api/admin/token
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN_HERE
{
"type": "yidong",
"description": "移动云盘token",
"token": "abc123xyz456"
}
### 获取单个网盘Token
# 替换下面的TOKEN_ID为实际的token ID
GET http://localhost:6400/api/admin/token/TOKEN_ID
Authorization: Bearer YOUR_TOKEN_HERE
### 更新网盘Token
# 替换下面的TOKEN_ID为实际的token ID
PUT http://localhost:6400/api/admin/token/TOKEN_ID
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN_HERE
{
"description": "更新后的描述",
"token": "new-token-value"
}
### 删除网盘Token
# 替换下面的TOKEN_ID为实际的token ID
DELETE http://localhost:6400/api/admin/token/TOKEN_ID
Authorization: Bearer YOUR_TOKEN_HERE

View File

@@ -0,0 +1,25 @@
POST https://login.123pan.com/api/user/sign_in
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
App-Version: 3
Connection: keep-alive
Content-Type: application/json
LoginUuid: 694eff443c1896851f0fa32abbb8c6ec69a422aa21721f4556d1e9f07a568bee
Referer: https://login.123pan.com/
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/137.0.0.0 Safari/537.36 Edg/137.0.0.0
platform: web
sec-ch-ua: "Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
{
"passport": "",
"password": "",
"remember": true
}
###
POST http://

View File

@@ -119,3 +119,39 @@ Platform:web
{"ShareKey":"iaKtVv-6OECd","behavior":1}
### eaefamemdead
https://www.123684.com/b/api/share/get?2393880368=1749017805-9397896-3041863692&limit=100&next=0&orderBy=file_name&orderDirection=asc&shareKey=LH3rTd-pENed&SharePwd=qaiu&ParentFileId=0&Page=1&event=homeListFile&operateType=1
Referer: https://www.123684.com/s/LH3rTd-pENed
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
platform: web
sec-ch-ua: "Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
###
POST https://www.123684.com/b/api/file/batch_download_share_info?3697171543=1749019332-221279-1801740277
Content-Type: application/json;charset=UTF-8
{"ShareKey":"LH3rTd-pENed","fileIdList":[{"fileId":17525951}]}
###
https://www.123865.com/b/api/share/get?limit=100&next=-1&orderBy=file_name&orderDirection=asc&shareKey=iaKtVv-FTaCd&SharePwd=qaiu&ParentFileId=0&Page=1&event=homeListFile&operateType=1&OrderId=
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
App-Version: 3
Connection: keep-alive
DNT: 1
Host: www.123865.com
Referer: https://www.123865.com/s/iaKtVv-FTaCd?%E6%8F%90%E5%8F%96%E7%A0%81%3Aqaiu
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0
platform: web
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"