Compare commits

...

63 Commits

Author SHA1 Message Date
q
55e6227de0 优化docker脚本5 2025-09-10 18:23:58 +08:00
q
24a7395004 优化docker脚本4 2025-09-10 18:16:37 +08:00
q
b2a7187fc5 优化docker脚本3 2025-09-10 18:10:51 +08:00
q
ace7cdc88e 优化docker脚本2 2025-09-10 18:05:14 +08:00
q
2e909b5868 优化docker脚本 2025-09-10 17:53:49 +08:00
q
de78bcbc98 docker多平台支持,add 树莓派 2025-09-10 17:45:27 +08:00
q
c560f0e902 docker多平台支持 2025-09-10 17:27:44 +08:00
q
88860c9302 docker多平台支持 2025-09-10 17:21:33 +08:00
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
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
54 changed files with 4545 additions and 635 deletions

View File

@@ -16,8 +16,9 @@ permissions:
on:
push:
tags:
- '*'
branches: [ "main" ]
- '*' # 只有推送tag时才会触发构建
branches-ignore:
- '*' # 排除所有分支的提交
paths-ignore:
- 'bin/**'
- '.github/**'
@@ -27,7 +28,8 @@ on:
- '*.txt'
- '*.md'
pull_request:
branches: [ "main" ]
branches:
- "main"
jobs:
build:
@@ -78,6 +80,8 @@ 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
@@ -91,6 +95,7 @@ jobs:
with:
context: .
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
ghcr.io/qaiu/netdisk-fast-download:latest

View File

@@ -1,34 +0,0 @@
name: Update Release Badge
on:
push:
tags:
- 'v*' # 可按需调整
jobs:
update-badge:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Get latest tag
id: get_tag
run: echo "tag_name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Update README badge
run: |
TAG=${{ steps.get_tag.outputs.tag_name }}
BADGE="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=$TAG"
echo "Using badge: $BADGE"
# 替换 README 中 badge 行(标记行需特殊注释)
sed -i -E "s#(!\[release-badge\]\(.*\))#![release-badge]($BADGE)#" README.md
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "🔄 update release badge for ${{ steps.get_tag.outputs.tag_name }}" || echo "No changes"
git push

View File

@@ -1,7 +1,10 @@
FROM eclipse-temurin:17-alpine
FROM eclipse-temurin:17-jre
WORKDIR /app
# 安装 unzip
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
COPY ./web-service/target/netdisk-fast-download-bin.zip .
RUN unzip netdisk-fast-download-bin.zip && \

View File

@@ -3,7 +3,7 @@
</p>
<p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/{user}/{repo}/{workflow_file}?branch={tag}"></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>
@@ -13,12 +13,33 @@
# netdisk-fast-download 网盘分享链接云解析服务
# netdisk-fast-download 网盘分享链接云解析服务
QQ群1017480890
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。
[预览地址1](https://lz.qaiu.top)
[预览地址2](http://www.722shop.top:6401)
## 快速开始
命令行下载分享文件:
```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返回数据格式示例**
@@ -43,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)
@@ -62,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拼接的短地址标志短链`,所有规则参考示例。
@@ -76,23 +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,
@@ -109,7 +136,7 @@ json返回数据格式示例:
"timestamp": 1726637151902
}
```
2. 分享链接详情接口 /v2/linkInfo?url=分享链接
#### 2. 分享链接详情接口 /v2/linkInfo?url=分享链接
```json
{
"code": 200,
@@ -138,7 +165,7 @@ json返回数据格式示例:
"timestamp": 1736489219402
}
```
3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
#### 3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
/v2/getFileList?url=分享链接&pwd=分享密码
```json
@@ -153,21 +180,21 @@ json返回数据格式示例:
"fileIcon": null,
"size": 999,
"sizeStr": "999 M",
"fileType": "apk",
"fileType": "file/folder",
"filePath": null,
"createTime": "17 小时前",
"updateTime": null,
"createBy": null,
"description": null,
"downloadCount": null,
"downloadCount": "下载次数",
"panType": "lz",
"parserUrl": "下载链接",
"parserUrl": "下载链接/文件夹链接",
"extParameters": null
}
]
}
```
4. 解析次数统计接口 /v2/statisticsInfo
#### 4. 解析次数统计接口 /v2/statisticsInfo
```json
{
"code": 200,
@@ -262,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端口
@@ -285,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端口
@@ -383,12 +410,13 @@ Core模块集成Vert.x实现类似spring的注解式路由API
## 支持该项目
开源不易,用爱发电,本项目长期维护如果觉得有帮助, 可以请作者喝杯咖啡, 感谢支持
### 关于专属版
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联调云盘的解析支持
199元, 包含部署服务和首页定制, 需提供宝塔环境
可以提供功能定制开发, 加v价格详谈:
<p>wechat1: qaiu-cn</p>
<p>wechat2: imcoding_</p>
<p>qq: 197575894</p>
<p>wechat: imcoding_</p>
<!--
![image](https://github.com/qaiu/netdisk-fast-download/assets/29825328/54276aee-cc3f-4ebd-8973-2e15f6295819)

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

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

@@ -72,6 +72,9 @@ public class FileInfo {
//预览地址
private String previewUrl;
// 文件hash默认类型为md5
private String hash;
/**
* 扩展参数
*/
@@ -210,6 +213,15 @@ public class FileInfo {
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

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

@@ -202,17 +202,18 @@ public abstract class PanBase implements IPanTool {
try {
log.error(decompressGzip((Buffer) res.body()));
fail(decompressGzip((Buffer) res.body()));
throw new RuntimeException("响应不是JSON格式");
//throw new RuntimeException("响应不是JSON格式");
} catch (IOException ex) {
log.error("响应gzip解压失败");
fail("响应gzip解压失败: {}", ex.getMessage());
throw new RuntimeException("响应gzip解压失败", ex);
//throw new RuntimeException("响应gzip解压失败", ex);
}
} else {
log.error("解析失败: json格式异常: {}", res.bodyAsString());
fail("解析失败: json格式异常: {}", res.bodyAsString());
throw new RuntimeException("解析失败: json格式异常");
//throw new RuntimeException("解析失败: json格式异常");
}
return JsonObject.of();
}
}
@@ -233,8 +234,9 @@ public abstract class PanBase implements IPanTool {
}
} catch (Exception e) {
fail("解析失败: res格式异常");
throw new RuntimeException("解析失败: res格式异常");
//throw new RuntimeException("解析失败: res格式异常");
}
return null;
}
protected void complete(String url) {

View File

@@ -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\\.com|123865\\.com|123684\\.com|123912\\.com|123pan\\.cn)/s/(?<KEY>.+)(.html)?"),
compile("https://www\\.(" +
"123254\\.com|" +
"123957\\.com|" +
"123295\\.com|" +
"123panpay\\.com|" +
"123860\\.com|" +
"123pan\\.com|" +
"123245\\.com|" +
"123278\\.com|" +
"123842\\.com|" +
"123294\\.com|" +
"123865\\.com|" +
"123773\\.com|" +
"123624\\.com|" +
"123684\\.com|" +
"123641\\.com|" +
"123259\\.com|" +
"123912\\.com|" +
"123952\\.com|" +
"123652\\.com|" +
"123pan\\.cn|" +
"123635\\.com|" +
"123242\\.com|" +
"123795\\.com" +
")/s/(?<KEY>.+)(.html)?"),
"https://www.123pan.com/s/{shareKey}",
YeTool.class),
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
@@ -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

@@ -149,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")) {
@@ -179,7 +179,11 @@ public class FjTool extends PanBase {
return promise.future();
}
parse().onSuccess(id -> {
parserDir(id, shareId, promise);
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);
@@ -198,8 +202,14 @@ public class FjTool extends PanBase {
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
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->{
@@ -269,7 +279,10 @@ public class FjTool extends PanBase {
result.add(fileInfo);
});
promise.complete(result);
});
}).onFailure(failRes -> {
log.error("解析目录请求失败: {}", failRes.getMessage());
promise.fail(failRes);
});;
}
@Override

View File

@@ -158,7 +158,11 @@ public class IzTool extends PanBase {
return promise.future();
}
parse().onSuccess(id -> {
parserDir(id, shareId, promise);
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);
@@ -177,8 +181,14 @@ public class IzTool extends PanBase {
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
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->{
@@ -244,6 +254,9 @@ public class IzTool extends PanBase {
result.add(fileInfo);
});
promise.complete(result);
}).onFailure(failRes -> {
log.error("解析目录请求失败: {}", failRes.getMessage());
promise.fail(failRes);
});
}

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,11 +1,14 @@
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;
@@ -15,7 +18,9 @@ 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;
@@ -32,7 +37,7 @@ 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}";
@@ -97,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")
@@ -227,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

@@ -29,7 +29,7 @@
<org.reflections.version>0.10.2</org.reflections.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

@@ -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",
"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

@@ -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">

View File

@@ -1,378 +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.9_bate2</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><strong>目录解析支持 </strong>蓝奏云/蓝奏云优享/小飞机盘/123云盘接入中</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" @click="onSubmit">文件解析</el-button>
<!-- 目录解析-->
<el-button style="margin-left: 20px" @click="getFileList">目录解析</el-button>
<el-button style="margin-left: 20px" @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: {},
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)
}
});
},
getFileList() {
this.check()
this.isLoading = true
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'
})
const data = response.data.data
const panList = ["iz", "lz", "fj"];
if (!panList.includes(data.shareLinkInfo.type)) {
this.$message.error("当前网盘不支持目录解析")
}
let listUrl = `${window.location.origin}/list.html?url=${encodeURIComponent(this.link)}`
let apiUrl = new URL(data.apiLink).origin + `/v2/getFileList?url=${this.link}`;
// 动态添加密码参数
if (this.password) {
listUrl += `&pwd=${this.password}`;
apiUrl += `&pwd=${this.password}`;
}
this.$alert(
`目录解析API: <a href="${apiUrl}" target="_blank">${apiUrl}</a><br>打开文件列表: <a href="${listUrl}" target="_blank">点击这里</a>`,
'目录解析信息',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '确定',
type: 'info'
}
);
} 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;
@@ -382,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

@@ -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;
@@ -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;
@@ -44,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) {
@@ -54,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"));
@@ -81,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: 网盘域名地址
@@ -142,6 +155,39 @@ public class ParserApi {
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();

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

@@ -36,15 +36,15 @@ custom:
- ^cn\.qaiu\.lz\.web\.model\..*
# 限流配置
rateLimit:
# 是否启用限流
enable: true
# 限流的请求数
limit: 10
# 限流的时间窗口(单位秒)
timeWindow: 10
# 路径匹配规则
pathReg: ^/v2/.*
#rateLimit:
# # 是否启用限流
# enable: true
# # 限流的请求数
# limit: 10
# # 限流的时间窗口(单位秒)
# timeWindow: 10
# # 路径匹配规则
# pathReg: ^/v2/.*
# 数据源配置
@@ -63,7 +63,7 @@ cache:
# 该配置未使用后续加入其他Cache实现时区分类型
type: h2db
# 默认时长: 单位分钟,大部分网盘未严格验证,建议不要太大
defaultDuration: 59
defaultDuration: 5
# 具体网盘的缓存配置如果不加配置则不缓存每次请求都会请求网盘API格式网盘标识: 时长
duration:
ce: 5
@@ -93,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

@@ -135,3 +135,23 @@ POST https://www.123684.com/b/api/file/batch_download_share_info?3697171543=1749
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"