Compare commits

..

1 Commits

Author SHA1 Message Date
QAIU
9ded137539 jdk11兼容 2024-06-19 09:51:08 +08:00
208 changed files with 15054 additions and 15819 deletions

View File

@@ -1,28 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/java
{
"name": "Java",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/java:0-17",
"features": {
"ghcr.io/devcontainers/features/java:1": {
"version": "none",
"installMaven": "true",
"installGradle": "false"
},
"ghcr.io/devcontainers-contrib/features/ant-sdkman:2": {}
}
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "java -version",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

8
.gitattributes vendored
View File

@@ -1,8 +0,0 @@
# 文本文件使用 LF 换行符,适用于 Linux 和 macOS
*.sh text eol=lf
*.service text eol=lf
# Windows 执行的文件使用 CRLF 换行符
*.bat text eol=crlf
*.cmd text eol=crlf
bin/nfd-service-template.xml text eol=crlf

15
.github/FUNDING.yml vendored
View File

@@ -1,15 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: https://blog.qaiu.top/archives/da-shang-zhuan-yong # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -11,25 +11,12 @@ name: Java CI with Maven
# The API requires write permission on the repository to submit dependencies
permissions:
contents: write
packages: write
on:
push:
tags:
- '*' # 只有推送tag时才会触发构建
branches-ignore:
- '*' # 排除所有分支的提交
paths-ignore:
- 'bin/**'
- '.github/**'
- '.mvn/**'
- '.run/**'
- '.vscode/**'
- '*.txt'
- '*.md'
branches: [ "main" ]
pull_request:
branches:
- "main"
branches: [ "main" ]
jobs:
build:
@@ -38,64 +25,21 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v4
with:
node-version: '18'
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build Frontend
run: cd web-front && yarn install && yarn run build
- name: Build with Maven
run: mvn -B package --file pom.xml
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@v3
if: github.event_name != 'pull_request'
with:
ignore-maven-wrapper: true
# - uses: release-drafter/release-drafter@v5
# env:
# GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
path: web-service/target/netdisk-fast-download-bin.zip
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract git tag
id: tag
run: |
GIT_TAG=$(git tag --points-at HEAD | head -n 1)
echo "tag=$GIT_TAG" >> $GITHUB_OUTPUT
- name: Build and push Docker image
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
ghcr.io/qaiu/netdisk-fast-download:latest

1
.gitignore vendored
View File

@@ -39,4 +39,3 @@ gradlew.bat
unused.txt
/web-service/src/main/generated/
/db
/webroot/nfd-front/

View File

@@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="AppMain" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="cn.qaiu.lz.AppMain" />
<module name="web-service" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="cn.qaiu.lz.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

70
.vscode/launch.json vendored
View File

@@ -1,70 +0,0 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Current File",
"request": "launch",
"mainClass": "${file}"
},
{
"type": "java",
"name": "StringCase",
"request": "launch",
"mainClass": "cn.qaiu.vx.core.util.StringCase",
"projectName": "core"
},
{
"type": "java",
"name": "FCURLParser",
"request": "launch",
"mainClass": "cn.qaiu.parser.FCURLParser",
"projectName": "parser"
},
{
"type": "java",
"name": "QkTool",
"request": "launch",
"mainClass": "cn.qaiu.parser.impl.QkTool",
"projectName": "parser"
},
{
"type": "java",
"name": "WebClientExample",
"request": "launch",
"mainClass": "qaiu.web.test.WebClientExample",
"projectName": "parser"
},
{
"type": "java",
"name": "AppMain",
"request": "launch",
"mainClass": "cn.qaiu.lz.AppMain",
"projectName": "web-service"
},
{
"type": "java",
"name": "TestJs",
"request": "launch",
"mainClass": "cn.qaiu.web.test.TestJs",
"projectName": "web-service"
},
{
"type": "java",
"name": "TestOS",
"request": "launch",
"mainClass": "cn.qaiu.web.test.TestOS",
"projectName": "web-service"
},
{
"type": "java",
"name": "WebProxyExamples",
"request": "launch",
"mainClass": "cn.qaiu.web.test.WebProxyExamples",
"projectName": "web-service"
}
]
}

View File

@@ -1,4 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
"java.compile.nullAnalysis.mode": "automatic"
}

View File

@@ -1,14 +0,0 @@
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY ./web-service/target/netdisk-fast-download-bin.zip .
RUN unzip netdisk-fast-download-bin.zip && \
mv netdisk-fast-download/* ./ && \
rm netdisk-fast-download-bin.zip && \
chmod +x run.sh
EXPOSE 6400 6401
ENTRYPOINT ["sh", "run.sh"]

364
README.md
View File

@@ -1,212 +1,109 @@
<p align="center">
<img src="https://github.com/user-attachments/assets/87401aae-b0b6-4ffb-bbeb-44756404d26f" alt="项目预览图" />
</p>
云盘解析服务 (nfd云解析)
预览地址 https://lz.qaiu.top
**注意: 请不要过度依赖lz.qaiu.top预览地址服务建议本地搭建或者云服务器自行搭建。
解析次数过多IP会被部分网盘厂商限制不推荐做公共解析。**
<p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b8a&style=flat"></a>
<a href="https://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>
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
</p>
[![Java CI with Maven](https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml/badge.svg)](https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml)
[![jdk](https://img.shields.io/badge/jdk-%3E%3D17-blue)](https://www.oracle.com/cn/java/technologies/downloads/)
[![vert.x](https://img.shields.io/badge/vert.x-4.5.6-blue)](https://vertx-china.github.io/)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/qaiu/netdisk-fast-download)](https://github.com/qaiu/netdisk-fast-download/releases/tag/0.1.6-releases)
## 项目介绍
网盘直链解析工具能把网盘分享下载链接转化为直链,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享。
# netdisk-fast-download 网盘分享链接云解析服务
QQ群1017480890
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。
## 快速开始
命令行下载分享文件:
```shell
curl -LOJ "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
```
或者使用wget:
```shell
wget -O bilibili.mp4 "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
```
或者使用浏览器[直接访问](https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4):
```
### 调用演示站下载:
https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234
### 调用演示站预览:
https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4
```
## 预览地址
[预览地址1](https://lz.qaiu.top)
[预览地址2](http://www.722shop.top:6401)
[天翼云盘大文件解析限时开放](https://189.qaiu.top)
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
**小飞机解析有IP限制多数云服务商的大陆IP会被拦截可以自行配置代理和本程序无关**
**注意: 请不要过度依赖lz.qaiu.top预览地址服务建议本地搭建或者云服务器自行搭建。解析次数过多IP会被部分网盘厂商限制不推荐做公共解析。**
*重要声明:本项目仅供学习参考;请不要将此项目用于任何商业用途,否则可能带来严重的后果。*
## 网盘支持情况:
> 20230905 奶牛云直链做了防盗链需加入请求头Referer: https://cowtransfer.com/
> 20230824 123云盘解析大文件(>100MB)失效,需要登录
> 20230722 UC网盘解析失效需要登录
网盘名称-网盘标识:
`网盘名称(网盘标识):`
- [蓝奏云-lz](https://pc.woozooo.com/)
- [蓝奏云优享-iz](https://www.ilanzou.com/)
- [奶牛快传-cow](https://cowtransfer.com/)
- [移动云云空间-ec](https://www.ecpan.cn/web)
- [小飞机网盘-fj](https://www.feijipan.com/)
- [亿方云-fc](https://www.fangcloud.com/)
- [123云盘-ye](https://www.123pan.com/)
- ~[115网盘(失效)-p115](https://115.com/)~
- [118网盘(已停服)-p118](https://www.118pan.com/)
- [文叔叔-ws](https://www.wenshushu.cn/)
- [联想乐云-le](https://lecloud.lenovo.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)
- [酷我音乐分享链接-mkws](https://kuwo.cn)
- [QQ音乐分享链接-mqqs](https://y.qq.com)
- 咪咕音乐分享链接(开发中)
- [Cloudreve自建网盘-ce](https://github.com/cloudreve/Cloudreve)
- ~[微雨云存储-pvvy](https://www.vyuyun.com/)~
- [超星云盘(需要referer: https://pan-yz.chaoxing.com)-pcx](https://pan-yz.chaoxing.com)
- Google云盘-pgd
- Onedrive-pod
- Dropbox-pdp
- iCloud-pic
### 仅专属版提供
- [移动云盘-p139](https://yun.139.com/)
- [联通云盘-pwo](https://pan.wo.cn/)
- [天翼云盘-p189](https://cloud.189.cn/)
- [蓝奏云 (lz)](https://pc.woozooo.com/)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析
- [蓝奏云优享 (iz)](https://www.ilanzou.com/)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析
- [奶牛快传 (cow)](https://cowtransfer.com/)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析
- [移动云云空间 (ec)](https://www.ecpan.cn/web)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析
- [小飞机网盘 (fj)](https://www.feijipan.com/)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析
- [亿方云 (fc)](https://www.fangcloud.com/)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析
- [123云盘 (ye)](https://www.123pan.com/)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析
- [文叔叔 (ws)](https://www.wenshushu.cn/)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析
- [Cloudreve自建网盘 (ce)](https://github.com/cloudreve/Cloudreve)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析
- [QQ邮箱 (qq) 暂不可用-存在cookie问题](https://wx.mail.qq.com/)
- [ ] 登录, 上传, 下载, 分享
- [X] 直链解析(用户无法直接使用直链)
- [夸克网盘 (qk) 寄了](https://pan.quark.cn/)
- [UC网盘 (uc) 寄了](https://fast.uc.cn/)
## API接口说明
your_host指的是您的域名或者IP实际使用时替换为实际域名或者IP端口默认6400可以使用nginx代理来做域名访问。
解析方式分为两种类型直接跳转下载文件和获取下载链接,
每一种都提供了两种接口形式: `通用接口parser?url=``网盘标志/分享key拼接的短地址标志短链`,所有规则参考示例。
- 通用接口: `/parser?url=分享链接&pwd=密码` 没有分享密码去掉&pwd参数;
- 标志短链: `/d/网盘标识/分享key@密码` 没有分享密码去掉@密码;
- 直链JSON: `/json/网盘标识/分享key@密码``/json/parser?url=分享链接&pwd=密码`
**TODO:**
- 登录接口, 文件上传/下载/分享后端接口
- 短地址服务
- 前端界面(建设中...)
### API接口说明
your_host指的是您的域名或者IP实际使用时替换为实际域名或者IP端口默认6400可以使用nginx代理来做域名访问。
解析方式分为两种类型直接跳转下载链接和获取下载链接(JSON),每一种都提供了两种接口形式parser和网盘标志/分享key拼接的短地址标志短链所有规则参考示例。
- 通用接口: `/parser?url=分享链接`加密分享需要加上参数pwd=密码;
- 标志短链: `/网盘标识/分享key` 在分享Key后面加上@密码;
- 直链JSON: `通用接口``标志短链`前加上`/json` 加密分享的密码规则同上;
- 网盘标识参考上面网盘支持情况
- 当带有分享密码时需要加上密码参数(pwd)
- 括号内是可选内容: 表示当带有分享密码时需要加上密码参数
- 移动云云空间,小飞机网盘的加密分享的密码可以忽略
- 移动云空间分享key取分享链接中的data参数,比如`&data=xxx`的参数就是xxx
API规则:
> 建议使用UrlEncode编码分享链接
规则示例:
```
1. 解析并自动302跳转
http://your_host/parser?url=分享链接&pwd=xxx
http://your_host/parser?url=UrlEncode(分享链接)&pwd=xxx
http://your_host/d/网盘标识/分享key@分享密码
1. 解析并自动302跳转 :
http://your_host/parser?url=分享链接(&pwd=xxx)
http://your_host/网盘标识/分享key(@分享密码)
2. 获取解析后的直链--JSON格式
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
http://your_host/json/parser?url=分享链接(&pwd=xxx)
http://your_host/json/网盘标识/分享key(@分享密码)
3. 需要特殊处理的网盘分享:
1. 移动云云空间(ec)使用parser?url= 解析时因为分享链接比较特殊(链接带有参数且含有#符号)所以要么对#进行转义%23要么直接去掉# 或者URL直接是主机名+'/'跟一个data参数
比如 http://your_host/parser?url=https://www.ecpan.cn/web//yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data=81027a5c99af5b11ca004966c945cce6W9Bf2&isShare=1
http://your_host/parser?url=https://www.ecpan.cn/web/%23/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data=81027a5c99af5b11ca004966c945cce6W9Bf2&isShare=1
http://your_host/parser?url=https://www.ecpan.cn/&data=81027a5c99af5b11ca004966c945cce6W9Bf2&isShare=1
2. Cloudreve自建网盘解析规则:
1. 标志短链: 根据网盘使用https和http选择 http://your_host/ce/https_网盘域名_s_wDz5TK 或 http://your_host/ce/http_网盘域名_s_wDz5TK
网盘域名指的是Cloudreve搭建网盘的主域名比如pan.huang1111.cn如果存在子路径需要将/替换为_是否存在子路径看分享链接格式是否是//网盘域名/子路径/s/xxx一般不存在子路径网盘域名/s/xxx
比如: http://127.0.0.1:6400/ce/https_pan.huang1111.cn_s_wDz5TK
2. parser接口 -> http://your_host/parser?url=分享链接(&pwd=xxx)
比如: http://127.0.0.1:6400/parser?url=https://pan.huang1111.cn/s/wDz5TK
### json接口说明
#### 1. 文件解析:/json/parser?url=分享链接&pwd=xxx
json返回数据格式示例:
`shareKey`: 全局分享key
`directLink`: 下载链接
`cacheHit`: 是否为缓存链接
`expires`: 缓存到期时间
```json
{
"code": 200,
"msg": "success",
"success": true,
"count": 0,
"data": {
"shareKey": "lz:xxx",
"directLink": "下载直链",
"cacheHit": true,
"expires": "2024-09-18 01:48:02",
"expiration": 1726638482825
},
"timestamp": 1726637151902
}
```
#### 2. 分享链接详情接口 /v2/linkInfo?url=分享链接
json返回数据格式示例:
```json
{
"code": 200,
"msg": "success",
"success": true,
"count": 0,
"data": {
"downLink": "https://lz.qaiu.top/d/fj/xx",
"apiLink": "https://lz.qaiu.top/json/fj/xx",
"cacheHitTotal": 5,
"parserTotal": 2,
"sumTotal": 7,
"shareLinkInfo": {
"shareKey": "xx",
"panName": "小飞机网盘",
"type": "fj",
"sharePassword": "",
"shareUrl": "https://share.feijipan.com/s/xx",
"standardUrl": "https://www.feijix.com/s/xx",
"otherParam": {
"UA": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
},
"cacheKey": "fj:xx"
}
},
"timestamp": 1736489219402
}
```
#### 3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
/v2/getFileList?url=分享链接&pwd=分享密码
```json
{
"code": 200,
"msg": "success",
"success": true,
"data": [
{
"fileName": "xxx",
"fileId": "xxx",
"fileIcon": null,
"size": 999,
"sizeStr": "999 M",
"fileType": "file/folder",
"filePath": null,
"createTime": "17 小时前",
"updateTime": null,
"createBy": null,
"description": null,
"downloadCount": "下载次数",
"panType": "lz",
"parserUrl": "下载链接/文件夹链接",
"extParameters": null
}
]
}
```
#### 4. 解析次数统计接口 /v2/statisticsInfo
```json
{
"code": 200,
"msg": "success",
"success": true,
"count": 0,
"data": {
"parserTotal": 320508,
"cacheTotal": 5957910,
"total": 6278418
},
"timestamp": 1736489378770
"data": "https://下载链接",
"timestamp": 1690733953927
}
```
@@ -279,62 +176,10 @@ mvn package
```
打包好的文件位于 web-service/target/netdisk-fast-download-bin.zip
## Linux服务部署
### Docker 部署Main分支
#### 海外服务器Docker部署
```shell
# 创建目录
mkdir -p netdisk-fast-download
cd netdisk-fast-download
# 拉取镜像
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: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:latest
# 反代6401端口
# 升级容器
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --run-once netdisk-fast-download
```
#### 国内Docker部署
```shell
# 创建目录
mkdir -p netdisk-fast-download
cd netdisk-fast-download
# 拉取镜像
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: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:latest
# 反代6401端口
# 升级容器
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --run-once netdisk-fast-download
```
### 宝塔部署指引 -> [点击进入宝塔部署教程](https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng)
### Linux命令行部署
> 注意: netdisk-fast-download.service中的ExecStart的路径改为实际路径
```shell
cd ~
wget -O netdisk-fast-download.zip https://github.com/qaiu/netdisk-fast-download/releases/download/0.1.8-release-fixed2/netdisk-fast-download-bin-fixed2.zip
wget -O netdisk-fast-download.zip https://github.com/qaiu/netdisk-fast-download/releases/download/0.1.7-release-fixed2/netdisk-fast-download-bin-fixed2.zip
unzip netdisk-fast-download-bin.zip
cd netdisk-fast-download
bash service-install.sh
@@ -366,36 +211,15 @@ bash service-install.sh
如果不想使用服务运行可以直接运行run.bat
> 注意: 如果jdk环境变量的java版本不是17请修改nfd-service-template.xml中的java命令的路径改为实际路径
## 相关配置说明
## 0.1.8 开发计划
- Docker部署
- 联想乐云解析
- 直链缓存
- 日志优化
resources目录下包含服务端配置文件 配置文件自带说明,具体请查看配置文件内容,
app-dev.yml 可以配置解析服务相关信息, 包括端口,域名,缓存时长等
server-proxy.yml 可以配置代理服务运行的相关信息, 包括前端反向代理端口,路径等
### ip代理配置说明
有时候解析量很大IP容易被ban这时候可以使用其他服务器搭建nfd-proxy代理服务。
修改配置文件:
app-dev.yml
```yaml
proxy:
- panTypes: pgd,pdb,pod # 网盘标识
type: http # 支持http/socks4/socks5
host: 127.0.0.1 # 代理IP
port: 7890 # 端口
username: # 用户名
password: # 密码
```
nfd-proxy搭建http代理服务器
参考https://github.com/nfd-parser/nfd-proxy
## 0.1.9 开发计划
- 目录解析(专属版)
- 带cookie/token参数解析大文件(专属版)
**技术栈:**
Jdk17+Vert.x4
Jdk17+Vert.x4.4.1
Core模块集成Vert.x实现类似spring的注解式路由API
@@ -403,26 +227,12 @@ Core模块集成Vert.x实现类似spring的注解式路由API
[![Star History Chart](https://api.star-history.com/svg?repos=qaiu/netdisk-fast-download&type=Date)](https://star-history.com/#qaiu/netdisk-fast-download&Date)
## **免责声明**
- 用户在使用本项目时,应自行承担风险,并确保其行为符合当地法律法规及网盘服务提供商的使用条款。
- 开发者不对用户因使用本项目而导致的任何后果负责,包括但不限于数据丢失、隐私泄露、账号封禁或其他任何形式的损害。
## 支持该项目
开源不易,用爱发电,本项目长期维护如果觉得有帮助, 可以请作者喝杯咖啡, 感谢支持
### 关于专属版
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联调云盘的解析支持
199元, 包含部署服务和首页定制, 需提供宝塔环境
可以提供功能定制开发, 加v价格详谈:
<p>qq: 197575894</p>
<p>wechat: imcoding_</p>
<!--
本项目长期维护如果觉得有帮助, 可以请作者喝杯咖啡, 感谢支持
![image](https://github.com/qaiu/netdisk-fast-download/assets/29825328/54276aee-cc3f-4ebd-8973-2e15f6295819)
[手机端支付宝打赏跳转链接](https://qr.alipay.com/fkx01882dnoxxtjenhlxt53)
-->

22
bin/build.bat Normal file
View File

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

View File

@@ -7,7 +7,7 @@ Wants=network-online.target
[Service]
Type=simple
# User=USER
# 需要JDK17及以上版本 注意修改为自己的路径
# 需要JDK11及以上版本 注意修改为自己的路径
ExecStart=/root/java/jdk-17.0.2/bin/java -server -Xmx128m -jar /root/java/netdisk-fast-download/netdisk-fast-download.jar
ExecStop=/bin/kill -s QUIT $MAINPID
Restart=always

View File

@@ -1,86 +0,0 @@
#!/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

@@ -4,7 +4,7 @@
<name>netdisk-fast-download</name>
<description>netdisk fast download service</description>
<executable>java</executable>
<arguments> -server -Xmx128m -Dfile.encoding=utf8 -jar ${jar} </arguments>
<arguments> -server -Xmx128m -jar ${jar} </arguments>
<logpath>${dd}\logs</logpath>
<log mode="roll-by-time">
<pattern>yyyyMMdd</pattern>

View File

@@ -1,4 +1,6 @@
@echo off && @chcp 65001 > nul
:: 需要JDK11及以上版本和Windows环境变量已配置jdk的路径
pushd %~dp0
set LIB_DIR=%~dp0
for /f "delims=X" %%i in ('dir /b %LIB_DIR%\netdisk-fast-download.jar') do (

View File

@@ -12,7 +12,7 @@
<artifactId>core-database</artifactId>
<properties>
<java.version>17</java.version>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>2.0.5</slf4j.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
@@ -41,7 +41,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
@@ -59,18 +59,6 @@
<artifactId>vertx-jdbc-client</artifactId>
<version>${vertx.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.2.0</version>
</dependency>
<!-- PG驱动-->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
</dependencies>
</project>

View File

@@ -21,6 +21,7 @@ public @interface Constraint {
boolean notNull() default false;
/**
* 唯一键约束 TODO 待实现
* @return 唯一键约束
*/
String uniqueKey() default "";
@@ -31,7 +32,7 @@ public @interface Constraint {
*/
String defaultValue() default "";
/**
* 默认值是否是函数 like value=NOW()
* 默认值是否是函数
* @return false 不是函数
*/
boolean defaultValueIsFunction() default false;

View File

@@ -1,67 +0,0 @@
package cn.qaiu.db.ddl;
import cn.qaiu.db.pool.JDBCPoolInit;
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CreateDatabase {
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
/**
* 解析数据库URL获取数据库名
* @param url 数据库URL
* @return 数据库名
*/
public static String getDatabaseName(String url) {
// 正则表达式匹配数据库名
String regex = "jdbc:mysql://[^/]+/(\\w+)(\\?.*)?";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
return matcher.group(1);
} else {
throw new IllegalArgumentException("Invalid database URL: " + url);
}
}
/**
* 使用JDBC原生方法创建数据库
* @param url 数据库连接URL
* @param user 数据库用户名
* @param password 数据库密码
*/
public static void createDatabase(String url, String user, String password) {
String dbName = getDatabaseName(url);
LOGGER.info(">>>>>>>>>>> 创建数据库:'{}' <<<<<<<<<<<< ", dbName);
// 去掉数据库名构建不带数据库名的URL
String baseUrl = url.substring(0, url.lastIndexOf("/") + 1) + "?characterEncoding=UTF-8&useUnicode=true";
try (Connection conn = DriverManager.getConnection(baseUrl, user, password);
Statement stmt = conn.createStatement()) {
// 创建数据库
stmt.executeUpdate("CREATE DATABASE IF NOT EXISTS " + dbName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
LOGGER.info(">>>>>>>>>>> 数据库'{}'创建成功 <<<<<<<<<<<<", dbName);
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void createDatabase(JsonObject dbConfig) {
createDatabase(
dbConfig.getString("jdbcUrl"),
dbConfig.getString("username"),
dbConfig.getString("password")
);
}
}

View File

@@ -6,9 +6,7 @@ import io.vertx.codegen.format.CamelCase;
import io.vertx.codegen.format.Case;
import io.vertx.codegen.format.LowerCamelCase;
import io.vertx.codegen.format.SnakeCase;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.sqlclient.Pool;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.templates.annotations.Column;
import io.vertx.sqlclient.templates.annotations.RowMapped;
import org.apache.commons.lang3.StringUtils;
@@ -16,7 +14,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.util.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 创建表
@@ -24,312 +24,154 @@ import java.util.*;
* @author <a href="https://qaiu.top">QAIU</a>
*/
public class CreateTable {
public static Map<Class<?>, String> javaProperty2SqlColumnMap = new HashMap<>() {{
// Java类型到SQL类型的映射
put(Integer.class, "INT");
put(Short.class, "SMALLINT");
put(Byte.class, "TINYINT");
put(Long.class, "BIGINT");
put(java.math.BigDecimal.class, "DECIMAL");
put(Double.class, "DOUBLE");
put(Float.class, "REAL");
put(Boolean.class, "BOOLEAN");
put(String.class, "VARCHAR");
put(Date.class, "TIMESTAMP");
put(java.time.LocalDateTime.class, "TIMESTAMP");
put(java.sql.Timestamp.class, "TIMESTAMP");
put(java.sql.Date.class, "DATE");
put(java.sql.Time.class, "TIME");
// 基本数据类型
put(int.class, "INT");
put(short.class, "SMALLINT");
put(byte.class, "TINYINT");
put(long.class, "BIGINT");
put(double.class, "DOUBLE");
put(float.class, "REAL");
put(boolean.class, "BOOLEAN");
}};
public static Map<Class<?>, String> javaProperty2SqlColumnMap = new HashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTable.class);
public static String UNIQUE_PREFIX = "idx_";
static {
javaProperty2SqlColumnMap.put(Integer.class, "INT");
javaProperty2SqlColumnMap.put(Short.class, "SMALLINT");
javaProperty2SqlColumnMap.put(Byte.class, "TINYINT");
javaProperty2SqlColumnMap.put(Long.class, "BIGINT");
javaProperty2SqlColumnMap.put(java.math.BigDecimal.class, "DECIMAL");
javaProperty2SqlColumnMap.put(Double.class, "DOUBLE");
javaProperty2SqlColumnMap.put(Float.class, "REAL");
javaProperty2SqlColumnMap.put(Boolean.class, "BOOLEAN");
javaProperty2SqlColumnMap.put(String.class, "VARCHAR");
javaProperty2SqlColumnMap.put(java.util.Date.class, "TIMESTAMP");
javaProperty2SqlColumnMap.put(java.time.LocalDateTime.class, "TIMESTAMP");
javaProperty2SqlColumnMap.put(java.sql.Timestamp.class, "TIMESTAMP");
javaProperty2SqlColumnMap.put(java.sql.Date.class, "DATE");
javaProperty2SqlColumnMap.put(java.sql.Time.class, "TIME");
javaProperty2SqlColumnMap.put(int.class, "INT");
javaProperty2SqlColumnMap.put(short.class, "SMALLINT");
javaProperty2SqlColumnMap.put(byte.class, "TINYINT");
javaProperty2SqlColumnMap.put(long.class, "BIGINT");
javaProperty2SqlColumnMap.put(double.class, "DOUBLE");
javaProperty2SqlColumnMap.put(float.class, "REAL");
javaProperty2SqlColumnMap.put(boolean.class, "BOOLEAN");
}
private static Case getCase(Class<?> clz) {
return switch (clz.getName()) {
case "io.vertx.codegen.format.CamelCase" -> CamelCase.INSTANCE;
case "io.vertx.codegen.format.SnakeCase" -> SnakeCase.INSTANCE;
case "io.vertx.codegen.format.LowerCamelCase" -> LowerCamelCase.INSTANCE;
default -> throw new UnsupportedOperationException();
};
}
public static List<String> getCreateTableSQL(Class<?> clz, JDBCType type) {
// 获取表名和主键
TableInfo tableInfo = extractTableInfo(clz, type);
// 构建表的SQL语句
List<String> sqlList = new ArrayList<>();
StringBuilder sb = new StringBuilder(50);
sb.append("CREATE TABLE IF NOT EXISTS ")
.append(tableInfo.quotationMarks).append(tableInfo.tableName).append(tableInfo.quotationMarks)
.append(" ( \r\n ");
// 处理字段并生成列定义
List<String> indexSQLs = new ArrayList<>();
processFields(clz, tableInfo, sb, indexSQLs);
// 去掉最后一个逗号并添加表尾部信息
String tableSQL = sb.substring(0, sb.lastIndexOf(",")) + tableInfo.endStr;
sqlList.add(tableSQL);
// 添加索引SQL
sqlList.addAll(indexSQLs);
return sqlList;
}
// 修改extractTableInfo方法处理没有Table注解时默认使用id字段作为主键
private static TableInfo extractTableInfo(Class<?> clz, JDBCType type) {
String quotationMarks;
String endStr;
if (type == JDBCType.MySQL) {
quotationMarks = "`";
endStr = ")ENGINE=InnoDB DEFAULT CHARSET=utf8;";
} else {
quotationMarks = "\"";
endStr = ");";
switch (clz.getName()) {
case "io.vertx.codegen.format.CamelCase":
return CamelCase.INSTANCE;
case "io.vertx.codegen.format.SnakeCase":
return SnakeCase.INSTANCE;
case "io.vertx.codegen.format.LowerCamelCase":
return LowerCamelCase.INSTANCE;
default:
throw new UnsupportedOperationException();
}
}
String primaryKey = null;
String tableName = null;
public static String getCreateTableSQL(Class<?> clz, JDBCType type) {
String quotationMarks = type == JDBCType.H2DB ? "\"" : "`";
String endStr = type == JDBCType.H2DB ? ");" : ")ENGINE=InnoDB DEFAULT CHARSET=utf8;";
// 判断类上是否有次注解
String primaryKey = null; // 主键
String tableName = null; // 表名
Case caseFormat = SnakeCase.INSTANCE;
// 判断类上是否有RowMapped注解
if (clz.isAnnotationPresent(RowMapped.class)) {
RowMapped annotation = clz.getAnnotation(RowMapped.class);
caseFormat = getCase(annotation.formatter());
Class<? extends Case> formatter = annotation.formatter();
caseFormat = getCase(formatter);
}
// 判断类上是否有Table注解
if (clz.isAnnotationPresent(Table.class)) {
// 获取类上的注解
Table annotation = clz.getAnnotation(Table.class);
tableName = StringUtils.isNotEmpty(annotation.value())
? annotation.value()
: LowerCamelCase.INSTANCE.to(caseFormat, clz.getSimpleName());
// 输出注解上的类名
String tableNameAnnotation = annotation.value();
if (StringUtils.isNotEmpty(tableNameAnnotation)) {
tableName = tableNameAnnotation;
} else {
tableName = LowerCamelCase.INSTANCE.to(caseFormat, clz.getSimpleName());
}
primaryKey = annotation.keyFields();
}
// 如果表名仍为null使用类名转下划线命名作为表名
if (StringUtils.isEmpty(tableName)) {
tableName = LowerCamelCase.INSTANCE.to(SnakeCase.INSTANCE, clz.getSimpleName());
}
// 如果主键为空默认使用id字段作为主键
if (StringUtils.isEmpty(primaryKey)) {
try {
clz.getDeclaredField("id");
primaryKey = "id";
} catch (NoSuchFieldException e) {
// 如果没有id字段不设置主键
primaryKey = null;
}
}
return new TableInfo(tableName, quotationMarks, endStr, primaryKey, caseFormat, type);
}
// 修改processFields方法处理索引
private static void processFields(Class<?> clz, TableInfo tableInfo, StringBuilder sb, List<String> indexSQLs) {
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
// 跳过无效字段
if (isIgnoredField(field)) {
String column;
int[] decimalSize = {22, 2};
int varcharSize = 255;
StringBuilder sb = new StringBuilder(50);
sb.append("CREATE TABLE IF NOT EXISTS ").append(quotationMarks).append(tableName).append(quotationMarks).append(" ( \r\n ");
boolean firstId = true;
for (Field f : fields) {
Class<?> paramType = f.getType();
String sqlType = javaProperty2SqlColumnMap.get(paramType);
if (f.getName().equals("serialVersionUID") || StringUtils.isEmpty(sqlType) || f.isAnnotationPresent(TableGenIgnore.class)) {
continue;
}
// 获取字段名和SQL类型
String column = LowerCamelCase.INSTANCE.to(tableInfo.caseFormat, field.getName());
String sqlType = javaProperty2SqlColumnMap.get(field.getType());
// 处理字段注解
column = processColumnAnnotation(field, column);
int[] decimalSize = {22, 2};
int varcharSize = 255;
if (field.isAnnotationPresent(Length.class)) {
Length length = field.getAnnotation(Length.class);
decimalSize = length.decimalSize();
varcharSize = length.varcharSize();
column = LowerCamelCase.INSTANCE.to(caseFormat, f.getName());
if (f.isAnnotationPresent(Column.class)) {
Column columnAnnotation = f.getAnnotation(Column.class);
//输出注解属性
if (StringUtils.isNotBlank(columnAnnotation.name())) {
column = columnAnnotation.name();
}
}
// 构建列定义
sb.append(tableInfo.quotationMarks).append(column).append(tableInfo.quotationMarks)
.append(" ").append(sqlType);
appendTypeLength(sqlType, sb, decimalSize, varcharSize);
appendConstraints(field, sb, tableInfo);
appendPrimaryKey(tableInfo.primaryKey, column, sb);
// 添加索引
appendIndex(tableInfo, indexSQLs, field);
sb.append(",\n ");
}
}
// 判断是否忽略字段
private static boolean isIgnoredField(Field field) {
return field.getName().equals("serialVersionUID")
|| StringUtils.isEmpty(javaProperty2SqlColumnMap.get(field.getType()))
|| field.isAnnotationPresent(TableGenIgnore.class);
}
// 处理Column注解
private static String processColumnAnnotation(Field field, String column) {
if (field.isAnnotationPresent(Column.class)) {
Column columnAnnotation = field.getAnnotation(Column.class);
if (StringUtils.isNotBlank(columnAnnotation.name())) {
column = columnAnnotation.name();
if (f.isAnnotationPresent(Length.class)) {
Length fieldAnnotation = f.getAnnotation(Length.class);
decimalSize = fieldAnnotation.decimalSize();
varcharSize = fieldAnnotation.varcharSize();
}
}
return column;
}
// 添加类型长度
private static void appendTypeLength(String sqlType, StringBuilder sb, int[] decimalSize, int varcharSize) {
if ("DECIMAL".equals(sqlType)) {
sb.append("(").append(decimalSize[0]).append(",").append(decimalSize[1]).append(")");
} else if ("VARCHAR".equals(sqlType)) {
sb.append("(").append(varcharSize).append(")");
}
}
// 添加约束
private static void appendConstraints(Field field, StringBuilder sb, TableInfo tableInfo) {
JDBCType type = tableInfo.dbType;
if (field.isAnnotationPresent(Constraint.class)) {
Constraint constraint = field.getAnnotation(Constraint.class);
if (constraint.notNull()) {
sb.append(" NOT NULL");
sb.append(quotationMarks).append(column).append(quotationMarks);
sb.append(" ").append(sqlType);
// 添加类型长度
if (sqlType.equals("DECIMAL")) {
sb.append("(").append(decimalSize[0]).append(",").append(decimalSize[1]).append(")");
}
String apostrophe = constraint.defaultValueIsFunction() ? "" : "'";
if (StringUtils.isNotEmpty(constraint.defaultValue())) {
sb.append(" DEFAULT ").append(apostrophe).append(constraint.defaultValue()).append(apostrophe);
if (sqlType.equals("VARCHAR")) {
sb.append("(").append(varcharSize).append(")");
}
if (constraint.autoIncrement()) {
if (type == JDBCType.PostgreSQL) {
// 需要移除字段类型(最后一个单词)
if (field.getType().equals(Integer.class)) {
sb.delete(sb.lastIndexOf(" "), sb.length());
sb.append(" SERIAL");
} else if (field.getType().equals(Long.class)) {
sb.delete(sb.lastIndexOf(" "), sb.length());
sb.append(" BIGSERIAL");
}
} else if (field.getType().equals(Integer.class) || field.getType().equals(Long.class)) {
if (f.isAnnotationPresent(Constraint.class)) {
Constraint constraintAnnotation = f.getAnnotation(Constraint.class);
if (constraintAnnotation.notNull()) {
//非空约束
sb.append(" NOT NULL");
}
String apostrophe = constraintAnnotation.defaultValueIsFunction() ? "" : "'";
if (StringUtils.isNotEmpty(constraintAnnotation.defaultValue())) {
//默认值约束
sb.append(" DEFAULT ").append(apostrophe).append(constraintAnnotation.defaultValue()).append(apostrophe);
}
if (constraintAnnotation.autoIncrement() && paramType.equals(Integer.class) || paramType.equals(Long.class)) {
////自增
sb.append(" AUTO_INCREMENT");
}
}
}
}
// 添加主键
private static void appendPrimaryKey(String primaryKey, String column, StringBuilder sb) {
if (StringUtils.isEmpty(primaryKey)) {
return;
}
if (primaryKey.equalsIgnoreCase(column)) {
sb.append(" PRIMARY KEY");
}
}
private static void appendIndex(TableInfo tableInfo, List<String> indexSQLs, Field field) {
if (!field.isAnnotationPresent(Constraint.class)) {
return;
}
Constraint constraint = field.getAnnotation(Constraint.class);
if (StringUtils.isEmpty(constraint.uniqueKey())) {
return;
}
String indexName = UNIQUE_PREFIX + tableInfo.tableName + "_" + constraint.uniqueKey();
String columnName = field.getName();
// 检查是否已有相同索引名称的索引
Optional<String> existingIndex = indexSQLs.stream()
.filter(sql -> sql.contains(tableInfo.quotationMarks + indexName + tableInfo.quotationMarks))
.findFirst();
if (existingIndex.isPresent()) {
// 如果存在相同索引名称,追加字段到索引定义中
String updatedIndex = existingIndex.get().replaceFirst(
"\\(([^)]+)\\)", // 匹配索引字段列表
"($1, " + tableInfo.quotationMarks + columnName + tableInfo.quotationMarks + ")"
);
indexSQLs.remove(existingIndex.get());
indexSQLs.add(updatedIndex);
} else {
// 如果不存在相同索引名称,创建新的索引
String indexSQL = String.format(
"CREATE UNIQUE INDEX %s %s%s%s ON %s%s%s (%s%s%s);",
tableInfo.dbType == JDBCType.MySQL ? "" : "IF NOT EXISTS",
tableInfo.quotationMarks, indexName, tableInfo.quotationMarks,
tableInfo.quotationMarks, tableInfo.tableName, tableInfo.quotationMarks,
tableInfo.quotationMarks, columnName, tableInfo.quotationMarks
);
indexSQLs.add(indexSQL);
}
}
// 表信息类
private record TableInfo(
String tableName, // 表名
String quotationMarks, // 引号或反引号
String endStr, // 表尾部信息
String primaryKey, // 主键字段
Case caseFormat, // 命名格式
JDBCType dbType // 数据库类型
) {
}
public static Future<Void> createTable(Pool pool, JDBCType type) {
Promise<Void> promise = Promise.promise();
Set<Class<?>> tableClasses = ReflectionUtil.getReflections().getTypesAnnotatedWith(Table.class);
if (tableClasses.isEmpty()) {
LOGGER.warn("Table model class not found");
promise.complete();
return promise.future();
}
List<Future<Object>> futures = new ArrayList<>();
for (Class<?> clazz : tableClasses) {
List<String> sqlList = getCreateTableSQL(clazz, type);
LOGGER.info("Class `{}` auto-generate table", clazz.getName());
for (String sql : sqlList) {
try {
pool.query(sql).execute().toCompletionStage().toCompletableFuture().join();
futures.add(Future.succeededFuture());
LOGGER.debug("Executed SQL:\n{}", sql);
} catch (Exception e) {
String message = e.getMessage();
if (message != null && message.contains("Duplicate key name")) {
LOGGER.warn("Ignoring duplicate key error: {}", message);
futures.add(Future.succeededFuture());
} else {
LOGGER.error("SQL Error: {}\nSQL: {}", message, sql);
futures.add(Future.failedFuture(e));
throw new RuntimeException(e); // Stop execution for other exceptions
}
if (StringUtils.isEmpty(primaryKey)) {
if (firstId) {//类型转换
sb.append(" PRIMARY KEY");
firstId = false;
}
} else {
if (primaryKey.equals(column.toLowerCase())) {
sb.append(" PRIMARY KEY");
}
}
sb.append(",\n ");
}
Future.all(futures).onSuccess(r -> promise.complete()).onFailure(promise::fail);
return promise.future();
String sql = sb.toString();
//去掉最后一个逗号
int lastIndex = sql.lastIndexOf(",");
sql = sql.substring(0, lastIndex) + sql.substring(lastIndex + 1);
return sql.substring(0, sql.length() - 1) + endStr;
}
public static void createTable(JDBCPool pool, JDBCType type) {
Set<Class<?>> tableClassList = ReflectionUtil.getReflections().getTypesAnnotatedWith(Table.class);
if (tableClassList.isEmpty()) LOGGER.info("Table model class not fount");
tableClassList.forEach(clazz -> {
String createTableSQL = getCreateTableSQL(clazz, type);
pool.query(createTableSQL).execute().onSuccess(
rs -> LOGGER.info("table auto generate:\n" + createTableSQL)
).onFailure(e -> {
LOGGER.error(e.getMessage() + " SQL: \n" + createTableSQL);
});
});
}
}

View File

@@ -1,13 +1,10 @@
package cn.qaiu.db.pool;
import cn.qaiu.db.ddl.CreateTable;
import cn.qaiu.db.ddl.CreateDatabase;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.jdbcclient.JDBCPool;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -20,29 +17,18 @@ import org.slf4j.LoggerFactory;
public class JDBCPoolInit {
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
private static final String providerClass = io.vertx.ext.jdbc.spi.impl.HikariCPDataSourceProvider.class.getName();
private JDBCPool pool = null;
JsonObject dbConfig;
Vertx vertx = VertxHolder.getVertxInstance();
String url;
private final JDBCType type;
private JDBCType type;
private static JDBCPoolInit instance;
public JDBCType getType() {
return type;
}
public JDBCPoolInit(Builder builder) {
this.dbConfig = builder.dbConfig;
this.url = builder.url;
this.type = JDBCType.getJDBCTypeByURL(builder.url);
if (StringUtils.isBlank(builder.dbConfig.getString("provider_class"))) {
builder.dbConfig.put("provider_class", providerClass);
}
this.type = builder.type;
}
public static Builder builder() {
@@ -56,10 +42,12 @@ public class JDBCPoolInit {
public static class Builder {
private JsonObject dbConfig;
private String url;
private JDBCType type;
public Builder config(JsonObject dbConfig) {
this.dbConfig = dbConfig;
this.url = dbConfig.getString("jdbcUrl");
this.type = JDBCUtil.getJDBCType(dbConfig.getString("driverClassName"));
return this;
}
@@ -71,34 +59,35 @@ public class JDBCPoolInit {
}
}
/**
* init h2db<br>
* 这个方法只允许调用一次
*/
synchronized public Future<Void> initPool() {
public void initPool() {
if (pool != null) {
LOGGER.error("pool 重复初始化");
return null;
return;
}
// 初始化数据库连接
// 初始化连接池
if (type == JDBCType.MySQL) {
CreateDatabase.createDatabase(dbConfig);
}
pool = JDBCPool.pool(vertx, dbConfig);
LOGGER.info("数据库连接初始化: URL=" + url);
return CreateTable.createTable(pool, type);
vertx.createSharedWorkerExecutor("sql-pool-init")
.executeBlocking(() -> {
// 初始化连接池
pool = JDBCPool.pool(vertx, dbConfig);
CreateTable.createTable(pool, type);
return "数据库连接初始化: URL=" + url;
})
.onSuccess(LOGGER::info)
.onFailure(Throwable::printStackTrace);
}
/**
* 获取连接池
*
* @return pool
*/
synchronized public JDBCPool getPool() {
public JDBCPool getPool() {
return pool;
}
}

View File

@@ -1,35 +1,9 @@
package cn.qaiu.db.pool;
import org.apache.commons.lang3.StringUtils;
/**
* @author <a href="https://qaiu.top">QAIU</a>
* @since 2023/10/10 14:06
* @date 2023/10/10 14:06
*/
public enum JDBCType {
// 添加驱动类型字段
MySQL("jdbc:mysql:"),
H2DB("jdbc:h2:"),
PostgreSQL("jdbc:postgresql:");
private final String urlPrefix; // JDBC URL 前缀
// 构造函数
JDBCType(String urlPrefix) {
this.urlPrefix = urlPrefix;
}
// 获取 JDBC URL 前缀
public String getUrlPrefix() {
return urlPrefix;
}
// 根据 JDBC URL 获取 JDBC 类型
public static JDBCType getJDBCTypeByURL(String jdbcURL) {
for (JDBCType jdbcType : values()) {
if (StringUtils.startsWithIgnoreCase(jdbcURL, jdbcType.getUrlPrefix())) {
return jdbcType;
}
}
throw new RuntimeException("不支持的SQL类型: " + jdbcURL);
}
MySQL, H2DB
}

View File

@@ -0,0 +1,18 @@
package cn.qaiu.db.pool;
/**
* @author <a href="https://qaiu.top">QAIU</a>
* @date 2023/10/10 14:05
*/
public class JDBCUtil {
public static JDBCType getJDBCType(String deviceName) {
switch (deviceName) {
case "com.mysql.cj.jdbc.Driver":
case "com.mysql.jdbc.Driver":
return JDBCType.MySQL;
case "org.h2.Driver":
return JDBCType.H2DB;
}
throw new RuntimeException("不支持的SQL驱动类型: " + deviceName);
}
}

View File

@@ -12,7 +12,6 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.build.timestamp.format>yyMMdd_HHmm</maven.build.timestamp.format>
</properties>
<dependencies>
@@ -61,6 +60,12 @@
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.melloware/commons-beanutils2 -->
<dependency>
<groupId>com.melloware</groupId>
<artifactId>commons-beanutils2</artifactId>
<version>${commons-beanutils2.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@@ -77,12 +82,6 @@
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -91,13 +90,6 @@
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<!-- 代码生成器 -->
<annotationProcessors>
<annotationProcessor>io.vertx.codegen.CodeGenProcessor</annotationProcessor>
</annotationProcessors>
<generatedSourcesDirectory>
${project.basedir}/src/main/generated
</generatedSourcesDirectory>
</configuration>
</plugin>
</plugins>

View File

@@ -1,13 +1,11 @@
package cn.qaiu.vx.core;
import cn.qaiu.vx.core.util.CommonUtil;
import cn.qaiu.vx.core.util.ConfigUtil;
import cn.qaiu.vx.core.util.VertxHolder;
import cn.qaiu.vx.core.verticle.ReverseProxyVerticle;
import cn.qaiu.vx.core.verticle.RouterVerticle;
import cn.qaiu.vx.core.verticle.ServiceVerticle;
import io.vertx.core.*;
import io.vertx.core.dns.AddressResolverOptions;
import io.vertx.core.impl.launcher.commands.VersionCommand;
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
@@ -54,9 +52,9 @@ public final class Deploy {
public void start(String[] args, Handler<JsonObject> handle) {
this.mainThread = Thread.currentThread();
this.handle = handle;
if (args.length > 0 && args[0].startsWith("app-")) {
if (args.length > 0) {
// 启动参数dev或者prod
path.append("-").append(args[0].replace("app-",""));
path.append("-").append(args[0]);
}
// 读取yml配置
@@ -89,21 +87,17 @@ public final class Deploy {
var calendar = Calendar.getInstance();
calendar.setTime(new Date());
var year = calendar.get(Calendar.YEAR);
var logoTemplate = """
Web Server powered by:\s
____ ____ _ _ _ \s
|_^^_| |_^^_| / |_ | | | | \s
\\ \\ / /.---. _ .--.`| |-' _ __ | |__| |_ \s
\\ \\ / // /__\\\\[ `/'`\\]| | [ \\ [ ]|____ _|\s
\\ V / | \\__., | | | |, _ > ' < _| |_ \s
\\_/ '.__.'[___] \\__/(_)[__]`\\_] |_____|\s
Version: %s; Framework version: %s; %s©%d.
""";
String logoTemplate = "Web Server powered by: \n" +
" ____ ____ _ _ _ \n" +
"|_^^_| |_^^_| / |_ | | | | \n" +
" \\ \\ / /.---. _ .--.`| |-' _ __ | |__| |_ \n" +
" \\ \\ / // /__\\\\[ `/'`\\]| | [ \\ [ ]|____ _|\n" +
" \\ V / | \\__., | | | |, _ > ' < _| |_ \n" +
" \\_/ '.__.'[___] \\__/(_)[__]`\\_] |_____|\n" +
" Version: %s; Framework version: %s; JDK11; %s©%d.\n\n";
System.out.printf(logoTemplate,
CommonUtil.getAppVersion(),
conf.getString("version_app"),
VersionCommand.getVersion(),
conf.getString("copyright"),
year
@@ -123,12 +117,6 @@ public final class Deploy {
var vertxOptions = vertxConfigELPS == 0 ?
new VertxOptions() : new VertxOptions(vertxConfig);
vertxOptions.setAddressResolverOptions(
new AddressResolverOptions().
addServer("114.114.114.114").
addServer("114.114.115.115").
addServer("8.8.8.8").
addServer("8.8.4.4"));
LOGGER.info("vertxConfigEventLoopPoolSize: {}, eventLoopPoolSize: {}, workerPoolSize: {}", vertxConfigELPS,
vertxOptions.getEventLoopPoolSize(),
vertxOptions.getWorkerPoolSize());
@@ -140,23 +128,20 @@ public final class Deploy {
localMap.put(GLOBAL_CONFIG, globalConfig);
localMap.put(CUSTOM_CONFIG, customConfig);
localMap.put(SERVER, globalConfig.getJsonObject(SERVER));
var future0 = vertx.createSharedWorkerExecutor("other-handle")
.executeBlocking(() -> {
handle.handle(globalConfig);
return "Other handle complete";
});
var future0 = vertx.createSharedWorkerExecutor("other-handle").executeBlocking(() -> {
handle.handle(globalConfig);
LOGGER.info("other handle complete");
return null;
});
future0.onSuccess(res -> {
LOGGER.info(res);
// 部署 路由、异步service、反向代理 服务
var future1 = vertx.deployVerticle(RouterVerticle.class, getWorkDeploymentOptions("Router"));
var future2 = vertx.deployVerticle(ServiceVerticle.class, getWorkDeploymentOptions("Service"));
var future3 = vertx.deployVerticle(ReverseProxyVerticle.class, getWorkDeploymentOptions("proxy"));
// 部署 路由、异步service、反向代理 服务
var future1 = vertx.deployVerticle(RouterVerticle.class, getWorkDeploymentOptions("Router"));
var future2 = vertx.deployVerticle(ServiceVerticle.class, getWorkDeploymentOptions("Service"));
var future3 = vertx.deployVerticle(ReverseProxyVerticle.class, getWorkDeploymentOptions("proxy"));
Future.all(future1, future2, future3)
.onSuccess(this::deployWorkVerticalSuccess)
.onFailure(this::deployVerticalFailed);
}).onFailure(e -> LOGGER.error("Other handle error", e));
Future.all(future1, future2, future3, future0)
.onSuccess(this::deployWorkVerticalSuccess)
.onFailure(this::deployVerticalFailed);
}
/**

View File

@@ -32,15 +32,12 @@ import org.slf4j.LoggerFactory;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static cn.qaiu.vx.core.util.ConfigConstant.ROUTE_TIME_OUT;
import static cn.qaiu.vx.core.verticle.ReverseProxyVerticle.REROUTE_PATH_PREFIX;
import static io.vertx.core.http.HttpHeaders.*;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME;
/**
* 路由映射, 参数绑定
@@ -73,19 +70,23 @@ public class RouterHandlerFactory implements BaseHttpApi {
public Router createRouter() {
// 主路由
Router mainRouter = Router.router(VertxHolder.getVertxInstance());
mainRouter.route().handler(ctx -> {
String realPath = ctx.request().uri();;
if (realPath.startsWith(REROUTE_PATH_PREFIX)) {
// vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath
String rePath = realPath.substring(REROUTE_PATH_PREFIX.length());
ctx.reroute(rePath);
return;
}
// 静态资源
String path = SharedDataUtil.getJsonConfig("server")
.getString("staticResourcePath");
if (!StringUtils.isEmpty(path)) {
// 静态资源
mainRouter.route("/*").handler(StaticHandler
.create(path)
.setCachingEnabled(true)
.setDefaultContentEncoding("UTF-8"));
}
mainRouter.route().handler(ctx -> {
LOGGER.debug("The HTTP service request address information ===>path:{}, uri:{}, method:{}",
ctx.request().path(), ctx.request().absoluteURI(), ctx.request().method());
ctx.response().headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
ctx.response().headers().add(DATE, LocalDateTime.now().format(ISO_LOCAL_DATE_TIME));
ctx.response().headers().add(ACCESS_CONTROL_ALLOW_METHODS, "POST, GET, OPTIONS, PUT, DELETE, HEAD");
ctx.response().headers().add(ACCESS_CONTROL_ALLOW_HEADERS, "X-PINGOTHER, Origin,Content-Type, Accept, " +
"X-Requested-With, Dev, Authorization, Version, Token");
@@ -111,7 +112,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
return Integer.compare(routeHandler2.order(), routeHandler1.order());
};
// 获取处理器类列表
List<Class<?>> sortedHandlers = handlers.stream().sorted(comparator).toList();
List<Class<?>> sortedHandlers = handlers.stream().sorted(comparator).collect(Collectors.toList());
for (Class<?> handler : sortedHandlers) {
try {
// 注册请求处理方法
@@ -152,7 +153,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
methodList.addAll(Stream.of(methods).filter(
method -> method.isAnnotationPresent(SockRouteMapper.class)
).toList());
).collect(Collectors.toList()));
// 依次注册处理方法
for (Method method : methodList) {
@@ -175,13 +176,8 @@ public class RouterHandlerFactory implements BaseHttpApi {
route.handler(ResponseTimeHandler.create());
route.handler(ctx -> handlerMethod(instance, method, ctx)).failureHandler(ctx -> {
if (ctx.response().ended()) return;
// 超时处理器状态码503
if (ctx.statusCode() == 503 || ctx.failure() == null) {
doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员", 500));
} else {
ctx.failure().printStackTrace();
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage(), 500));
}
ctx.failure().printStackTrace();
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage(), 500));
});
} else if (method.isAnnotationPresent(SockRouteMapper.class)) {
// websocket 基于sockJs

View File

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

View File

@@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.vertx.core.json.JsonObject;
import org.apache.commons.lang3.StringUtils;
import java.io.Serial;
import java.io.Serializable;
/**
@@ -17,7 +16,6 @@ import java.io.Serializable;
*/
public class JsonResult<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private static final int SUCCESS_CODE = 200;
@@ -30,10 +28,12 @@ public class JsonResult<T> implements Serializable {
private int code = SUCCESS_CODE;//状态码
private String msg = SUCCESS_MESSAGE; //消息
private String msg = SUCCESS_MESSAGE;//消息
private boolean success = true; //是否成功
private int count;
private T data;
private long timestamp = System.currentTimeMillis(); //时间戳
@@ -52,6 +52,20 @@ public class JsonResult<T> implements Serializable {
this.success = success;
}
public JsonResult(int code, String msg, boolean success, T data, int count) {
this(code, msg, success, data);
this.count = count;
}
public int getCount() {
return count;
}
public JsonResult<T> setCount(int count) {
this.count = count;
return this;
}
public int getCode() {
return code;
}
@@ -120,9 +134,20 @@ public class JsonResult<T> implements Serializable {
return new JsonResult<>(SUCCESS_CODE, msg, true, data);
}
// 响应成功消息和数据实体
public static <T> JsonResult<T> data(String msg, T data, int count) {
if (StringUtils.isEmpty(msg)) msg = SUCCESS_MESSAGE;
return new JsonResult<>(SUCCESS_CODE, msg, true, data, count);
}
// 响应数据实体
public static <T> JsonResult<T> data(T data) {
return new JsonResult<>(SUCCESS_CODE, SUCCESS_MESSAGE, true, data);
return new JsonResult<>(SUCCESS_CODE, SUCCESS_MESSAGE, true, data, 0);
}
// 响应数据实体
public static <T> JsonResult<T> data(T data, int count) {
return new JsonResult<>(SUCCESS_CODE, SUCCESS_MESSAGE, true, data, count);
}
// 响应成功消息

View File

@@ -1,7 +0,0 @@
/**
* ModuleGen cn.qaiu.vx.core
*/
@ModuleGen(name = "vertx-http-proxy", groupPackage = "cn.qaiu.vx.core", useFutures = true)
package cn.qaiu.vx.core;
import io.vertx.codegen.annotations.ModuleGen;

View File

@@ -3,6 +3,8 @@ package cn.qaiu.vx.core.util;
import cn.qaiu.vx.core.annotaions.HandleSortFilter;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import org.apache.commons.beanutils2.ConvertUtils;
import org.apache.commons.beanutils2.Converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -14,7 +16,6 @@ import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
@@ -98,6 +99,23 @@ public class CommonUtil {
return data;
}
/**
* 注册枚举转换器
*
* @param enums 枚举类
*/
@SafeVarargs
@SuppressWarnings({"unchecked", "rawtypes"})
public static void enumConvert(Class<? extends Enum>... enums) {
for (Class<? extends Enum> anEnum : enums) {
ConvertUtils.register(new Converter() {
public Object convert(Class type, Object value) {
return Enum.valueOf(anEnum, (String) value);
}
}, anEnum);
}
}
/**
* 处理其他配置
*
@@ -140,21 +158,4 @@ public class CommonUtil {
}
}).collect(Collectors.toSet());
}
private static String appVersion;
public static String getAppVersion() {
if (null == appVersion) {
Properties properties = new Properties();
try {
properties.load(CommonUtil.class.getClassLoader().getResourceAsStream("app.properties"));
if (!properties.isEmpty()) {
appVersion = properties.getProperty("app.version") + "build" + properties.getProperty("build");
}
} catch (IOException e) {
e.printStackTrace();
}
}
return appVersion;
}
}

View File

@@ -6,13 +6,6 @@ public interface ConfigConstant {
String EVENT_LOOP_POOL_SIZE = "eventLoopPoolSize";
String LOCAL = "local";
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

@@ -1,46 +0,0 @@
package cn.qaiu.vx.core.util;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import io.vertx.core.json.jackson.DatabindCodec;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* @author <a href="https://qaiu.top">QAIU</a>
* @date 2023/10/14 9:07
*/
public class JacksonConfig {
static {
// 通过该方法对mapper对象进行设置所有序列化的对象都将按改规则进行系列化
// Include.Include.ALWAYS 默认
// Include.NON_DEFAULT 属性为默认值不序列化
// Include.NON_EMPTY 属性为 空("" 或者为 NULL 都不序列化则返回的json是没有这个字段的。这样对移动端会更省流量
// Include.NON_NULL 属性为NULL 不序列化,就是为null的字段不参加序列化
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
ObjectMapper objectMapper = DatabindCodec.mapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDate.class,
new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalTime.class,
new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
objectMapper.registerModule(javaTimeModule);
LoggerFactory.getLogger(JacksonConfig.class).info("Global JacksonConfig complete.");
}
public static void nothing() {}
}

View File

@@ -1,10 +1,11 @@
package cn.qaiu.vx.core.util;
import io.vertx.core.MultiMap;
import io.vertx.core.json.JsonObject;
import org.apache.commons.beanutils2.BeanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
@@ -17,21 +18,29 @@ import java.util.Map;
public final class ParamUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ParamUtil.class);
public static Map<String, Object> multiMapToMap(MultiMap multiMap) {
public static Map<String, String> multiMapToMap(MultiMap multiMap) {
if (multiMap == null) return null;
Map<String, Object> map = new HashMap<>();
Map<String, String> map = new HashMap<>();
for (Map.Entry<String, String> entry : multiMap.entries()) {
map.put(entry.getKey(), entry.getValue());
}
return map;
}
public static <T> T multiMapToEntity(MultiMap multiMap, Class<T> tClass) {
Map<String, Object> map = multiMapToMap(multiMap);
if (map == null) {
return null;
public static <T> T multiMapToEntity(MultiMap multiMap, Class<T> tClass) throws NoSuchMethodException {
Map<String, String> map = multiMapToMap(multiMap);
T obj = null;
try {
obj = tClass.getDeclaredConstructor().newInstance();
BeanUtils.populate(obj, map);
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
LOGGER.error("实例化异常");
} catch (InvocationTargetException e2) {
e2.printStackTrace();
LOGGER.error("map2bean转换异常");
}
return new JsonObject(map).mapTo(tClass);
return obj;
}
public static MultiMap paramsToMap(String paramString) {

View File

@@ -12,8 +12,7 @@ import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
public class ResponseUtil {
public static void redirect(HttpServerResponse response, String url) {
response.putHeader(CONTENT_TYPE, "text/html; charset=utf-8")
.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
response.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
}
public static void redirect(HttpServerResponse response, String url, Promise<?> promise) {
@@ -27,20 +26,10 @@ public class ResponseUtil {
.end(jsonObject.encode());
}
public static void fireJsonObjectResponse(HttpServerResponse ctx, JsonObject jsonObject) {
ctx.putHeader(CONTENT_TYPE, "application/json; charset=utf-8")
.setStatusCode(200)
.end(jsonObject.encode());
}
public static <T> void fireJsonResultResponse(RoutingContext ctx, JsonResult<T> jsonResult) {
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
}
public static <T> void fireJsonResultResponse(HttpServerResponse ctx, JsonResult<T> jsonResult) {
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
}
public static void fireTextResponse(RoutingContext ctx, String text) {
ctx.response().putHeader(CONTENT_TYPE, "text/html; charset=utf-8").end(text);
}

View File

@@ -1,184 +0,0 @@
package cn.qaiu.vx.core.verticle;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.dns.AddressResolverOptions;
import io.vertx.core.http.*;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetClientOptions;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.ProxyOptions;
import java.util.Base64;
/**
*
*/
public class HttpProxyVerticle extends AbstractVerticle {
private HttpClient httpClient;
private NetClient netClient;
@Override
public void start() {
ProxyOptions proxyOptions = new ProxyOptions().setHost("127.0.0.1").setPort(7890);
// 初始化 HTTP 客户端,用于向目标服务器发送 HTTP 请求
HttpClientOptions httpClientOptions = new HttpClientOptions();
httpClient = vertx.createHttpClient(httpClientOptions.setProxyOptions(proxyOptions));
// 创建并启动 HTTP 代理服务器,监听指定端口
HttpServer server = vertx.createHttpServer(new HttpServerOptions().setClientAuth(ClientAuth.REQUIRED));
server.requestHandler(this::handleClientRequest);
// 初始化 NetClient用于在 CONNECT 请求中建立 TCP 连接隧道
netClient = vertx.createNetClient(new NetClientOptions()
.setProxyOptions(proxyOptions)
.setConnectTimeout(15000)
.setTrustAll(true));
// 启动 HTTP 代理服务器
server.listen(7891, ar -> {
if (ar.succeeded()) {
System.out.println("HTTP Proxy server started on port 7891");
} else {
System.err.println("Failed to start HTTP Proxy server: " + ar.cause());
}
});
}
// 处理 HTTP CONNECT 请求,用于代理 HTTPS 流量
private void handleConnectRequest(HttpServerRequest clientRequest) {
String[] uriParts = clientRequest.uri().split(":");
if (uriParts.length != 2) {
clientRequest.response().setStatusCode(400).end("Bad Request: Invalid URI format");
return;
}
// 解析目标主机和端口
String targetHost = uriParts[0];
int targetPort;
try {
targetPort = Integer.parseInt(uriParts[1]);
} catch (NumberFormatException e) {
clientRequest.response().setStatusCode(400).end("Bad Request: Invalid port");
return;
}
clientRequest.pause();
// 通过 NetClient 连接目标服务器并创建隧道
netClient.connect(targetPort, targetHost, connectionAttempt -> {
if (connectionAttempt.succeeded()) {
NetSocket targetSocket = connectionAttempt.result();
// 升级客户端连接到 NetSocket 并实现双向数据流
clientRequest.toNetSocket().onComplete(clientSocketAttempt -> {
if (clientSocketAttempt.succeeded()) {
NetSocket clientSocket = clientSocketAttempt.result();
// 设置双向数据流转发
clientSocket.handler(targetSocket::write);
targetSocket.handler(clientSocket::write);
// 关闭其中一方时关闭另一方
clientSocket.closeHandler(v -> targetSocket.close());
targetSocket.closeHandler(v -> clientSocket.close());
} else {
System.err.println("Failed to upgrade client connection to socket: " + clientSocketAttempt.cause().getMessage());
targetSocket.close();
clientRequest.response().setStatusCode(500).end("Internal Server Error");
}
});
} else {
System.err.println("Failed to connect to target: " + connectionAttempt.cause().getMessage());
clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to connect to target");
}
});
}
// 处理客户端的 HTTP 请求
private void handleClientRequest(HttpServerRequest clientRequest) {
String s = clientRequest.headers().get("Proxy-Authorization");
if (s == null) {
clientRequest.response().setStatusCode(403).end();
return;
}
String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
if (split.length > 1) {
System.out.println(split[0]);
System.out.println(split[1]);
// TODO
}
if (clientRequest.method() == HttpMethod.CONNECT) {
// 处理 CONNECT 请求
handleConnectRequest(clientRequest);
} else {
// 处理普通的 HTTP 请求
handleHttpRequest(clientRequest);
}
}
// 处理 HTTP 请求,转发至目标服务器并返回响应
private void handleHttpRequest(HttpServerRequest clientRequest) {
// 获取目标主机
String hostHeader = clientRequest.getHeader("Host");
if (hostHeader == null) {
clientRequest.response().setStatusCode(400).end("Host header is missing");
return;
}
String targetHost = hostHeader.split(":")[0];
int targetPort = 80; // 默认为 HTTP 的端口
clientRequest.pause(); // 暂停客户端请求的读取,避免数据丢失
httpClient.request(clientRequest.method(), targetPort, targetHost, clientRequest.uri())
.onSuccess(request -> {
clientRequest.resume(); // 恢复客户端请求的读取
// 逐个设置请求头
clientRequest.headers().forEach(header -> request.putHeader(header.getKey(), header.getValue()));
// 将客户端请求的 body 转发给目标服务器
clientRequest.bodyHandler(body -> request.send(body, ar -> {
if (ar.succeeded()) {
var response = ar.result();
clientRequest.response().setStatusCode(response.statusCode());
clientRequest.response().headers().setAll(response.headers());
response.body().onSuccess(b-> clientRequest.response().end(b));
} else {
clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to reach target");
}
}));
})
.onFailure(err -> {
err.printStackTrace();
clientRequest.response().setStatusCode(502).end("Bad Gateway: Request failed");
});
}
@Override
public void stop() {
// 停止 HTTP 客户端以释放资源
if (httpClient != null) {
httpClient.close();
}
}
/**
* TODO add Deploy
* @param args
*/
public static void main(String[] args) {
// 配置 DNS 解析器,使用多个 DNS 服务器来提升解析速度
Vertx vertx = Vertx.vertx(new VertxOptions()
.setAddressResolverOptions(new AddressResolverOptions()
.addServer("114.114.114.114")
.addServer("114.114.115.115")
.addServer("8.8.8.8")
.addServer("8.8.4.4")));
// 部署 Verticle 并启动动态 HTTP 代理服务器
vertx.deployVerticle(new HttpProxyVerticle());
}
}

View File

@@ -6,13 +6,13 @@ import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.WebSocket;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.StaticHandler;
import io.vertx.ext.web.handler.sockjs.SockJSHandler;
import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions;
import io.vertx.ext.web.proxy.handler.ProxyHandler;
import io.vertx.httpproxy.HttpProxy;
import org.apache.commons.lang3.StringUtils;
@@ -40,17 +40,14 @@ public class ReverseProxyVerticle extends AbstractVerticle {
.getJsonConfig(ConfigConstant.GLOBAL_CONFIG)
.getString("proxyConf");
private static final Future<JsonObject> CONFIG = ConfigUtil.readYamlConfig(PATH_PROXY_CONFIG);
private static final String DEFAULT_PATH_404 = "webroot/err/page404.html";
private static final String DEFAULT_PATH_404 = "webroot/err/404.html";
private static String serverName = "Vert.x-proxy-server"; //Server name in Http response header
public static String REROUTE_PATH_PREFIX = "/__rrvpspp"; //re_route_vert_proxy_server_path_prefix 硬编码
@Override
public void start(Promise<Void> startPromise) {
public void start(Promise<Void> startPromise) throws Exception {
CONFIG.onSuccess(this::handleProxyConfList);
// createFileListener
startPromise.complete();
}
@@ -77,24 +74,22 @@ public class ReverseProxyVerticle extends AbstractVerticle {
* @param proxyConf 代理配置
*/
private void handleProxyConf(JsonObject proxyConf) {
// page404 path
if (proxyConf.containsKey(
"page404")) {
// 404 path
if (proxyConf.containsKey("404")) {
System.getProperty("user.dir");
String path = proxyConf.getString("page404");
String path = proxyConf.getString("404");
if (StringUtils.isEmpty(path)) {
proxyConf.put("page404", DEFAULT_PATH_404);
proxyConf.put("404", DEFAULT_PATH_404);
} else {
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!new File(System.getProperty("user.dir") + path).exists()) {
proxyConf.put("page404", DEFAULT_PATH_404);
proxyConf.put("404", DEFAULT_PATH_404);
}
}
} else {
proxyConf.put("page404", DEFAULT_PATH_404);
proxyConf.put("404", DEFAULT_PATH_404);
}
final HttpClient httpClient = VertxHolder.getVertxInstance().createHttpClient();
@@ -116,10 +111,17 @@ public class ReverseProxyVerticle extends AbstractVerticle {
handleStatic(proxyConf.getJsonObject("static"), proxyRouter);
}
// Send page404 page
proxyRouter.errorHandler(404, ctx -> ctx.response().sendFile(proxyConf.getString("page404")));
// static server
if (proxyConf.containsKey("sock")) {
handleSock(proxyConf.getJsonArray("sock"), httpClient, proxyRouter);
}
HttpServer server = getHttpsServer(proxyConf);
// Send 404 page
proxyRouter.errorHandler(404, ctx -> {
ctx.response().sendFile(proxyConf.getString("404"));
});
HttpServer server = vertx.createHttpServer();
server.requestHandler(proxyRouter);
Integer port = proxyConf.getInteger("listen");
@@ -127,38 +129,6 @@ public class ReverseProxyVerticle extends AbstractVerticle {
server.listen(port);
}
private HttpServer getHttpsServer(JsonObject proxyConf) {
HttpServerOptions httpServerOptions = new HttpServerOptions();
if (proxyConf.containsKey("ssl")) {
JsonObject sslConfig = proxyConf.getJsonObject("ssl");
URL sslUrl = this.getClass().getClassLoader().getResource("");
if (sslUrl == null) {
throw new RuntimeException("SSL url not exist...");
}
if (sslConfig.containsKey("enable") && sslConfig.getBoolean("enable")) {
String sslCertificatePath = sslUrl.getPath() + sslConfig.getString("ssl_certificate");
String sslCertificateKeyPath = sslUrl.getPath() + sslConfig.getString("ssl_certificate_key");
LOGGER.info("enable ssl config. ");
httpServerOptions
.setSsl(true)
.setKeyCertOptions(
new PemKeyCertOptions()
.setKeyPath(sslCertificateKeyPath)
.setCertPath(sslCertificatePath)
).addEnabledSecureTransportProtocol(sslConfig.getString("ssl_protocols"));
String sslCiphers = sslConfig.getString("ssl_ciphers");
if (sslCiphers != null && !sslCiphers.isEmpty()) {
for (String s : sslCiphers.split(":")) {
httpServerOptions.addEnabledCipherSuite(s);
}
}
}
}
return vertx.createHttpServer(httpServerOptions);
}
/**
* 处理静态资源配置
*
@@ -175,12 +145,9 @@ public class ReverseProxyVerticle extends AbstractVerticle {
ctx.next();
});
StaticHandler staticHandler;
final StaticHandler staticHandler = StaticHandler.create();
if (staticConf.containsKey("root")) {
staticHandler = StaticHandler.create(staticConf.getString("root"));
} else {
staticHandler = StaticHandler.create();
staticHandler.setWebRoot(staticConf.getString("root"));
}
if (staticConf.containsKey("directory-listing")) {
staticHandler.setDirectoryListing(staticConf.getBoolean("directory-listing"));
@@ -211,7 +178,7 @@ public class ReverseProxyVerticle extends AbstractVerticle {
port = 80;
}
String originPath = url.getPath();
LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port);
LOGGER.debug("Conf(path, originPath, host, port) ----> {},{},{},{}", path, originPath, host, port);
// 注意这里不能origin多个代理地址, 一个实例只能代理一个origin
final HttpProxy httpProxy = HttpProxy.reverseProxy(httpClient);
@@ -222,21 +189,14 @@ public class ReverseProxyVerticle extends AbstractVerticle {
// 代理目标路径为空 就像nginx一样路径穿透 (相对路径)
if (StringUtils.isEmpty(originPath) || path.equals(originPath)) {
Route route = path.startsWith("~") ? proxyRouter.routeWithRegex(path.substring(1))
: proxyRouter.route(path);
route.handler(ProxyHandler.create(httpProxy));
proxyRouter.route(path + "*").handler(ProxyHandler.create(httpProxy));
} else {
// 配置 /api/, / => 请求 /api/test 代理后 /test
// 配置 /api/, /xxx => 请求 /api/test 代理后 /xxx/test
final String path0 = path;
final String originPath0 = REROUTE_PATH_PREFIX + originPath;
proxyRouter.route(originPath0 + "*").handler(ProxyHandler.create(httpProxy));
proxyRouter.route(path0 + "*").handler(ctx -> {
String realPath = ctx.request().uri();
if (realPath.startsWith(path0)) {
proxyRouter.route(originPath + "*").handler(ProxyHandler.create(httpProxy));
proxyRouter.route(path + "*").handler(ctx -> {
String realPath = ctx.request().path();
if (realPath.startsWith(path)) {
// vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath
String rePath = realPath.replaceAll("^" + path0, originPath0);
String rePath = realPath.replaceAll("^" + path, originPath);
ctx.reroute(rePath);
} else {
ctx.next();
@@ -250,4 +210,54 @@ public class ReverseProxyVerticle extends AbstractVerticle {
});
}
/**
* 处理websocket
*
* @param confList sock配置
* @param httpClient 客户端
* @param proxyRouter 代理路由
*/
private void handleSock(JsonArray confList, HttpClient httpClient, Router proxyRouter) {
// 代理规则
confList.stream().map(e -> (JsonObject) e).forEach(conf -> {
String origin = conf.getString("origin");
String path = conf.getString("path");
LOGGER.info("websocket proxy: {}, {}",origin,path);
SockJSHandlerOptions options = new SockJSHandlerOptions()
.setHeartbeatInterval(2000)
.setRegisterWriteHandler(true);
SockJSHandler sockJSHandler = SockJSHandler.create(VertxHolder.getVertxInstance(), options);
if (!path.startsWith("/")) {
path = "/" + path;
}
Router route = sockJSHandler.socketHandler(sock -> {
sock.handler(buffer -> {
Future<WebSocket> webSocketFuture = httpClient.webSocket(8086,"127.0.0.1",sock.uri());
webSocketFuture.onSuccess(s -> {
System.out.println(buffer.toString());
s.write(buffer).onSuccess(v -> {
s.handler(w->{
System.out.println("--------"+w.toString());
});
});
});
});
sock.endHandler(v -> {
});
sock.closeHandler(v -> {
});
});
proxyRouter.mountSubRouter("/real/serverApi/test", route);
});
}
}

View File

@@ -2,7 +2,6 @@ package cn.qaiu.vx.core.verticle;
import cn.qaiu.vx.core.handlerfactory.RouterHandlerFactory;
import cn.qaiu.vx.core.util.CommonUtil;
import cn.qaiu.vx.core.util.JacksonConfig;
import cn.qaiu.vx.core.util.SharedDataUtil;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
@@ -31,8 +30,6 @@ public class RouterVerticle extends AbstractVerticle {
private HttpServer server;
static {
LOGGER.info(JacksonConfig.class.getSimpleName() + " >> ");
JacksonConfig.nothing();
LOGGER.info("To start listening to port {} ......", port);
}

View File

@@ -1,89 +0,0 @@
package cn.qaiu.vx.core.verticle.conf;
import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.json.annotations.JsonGen;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.ProxyOptions;
import java.util.UUID;
@DataObject
@JsonGen(publicConverter = false)
public class HttpProxyConf {
public static final String DEFAULT_USERNAME = UUID.randomUUID().toString();
public static final String DEFAULT_PASSWORD = UUID.randomUUID().toString();
public static final Integer DEFAULT_PORT = 6402;
public static final Integer DEFAULT_TIMEOUT = 15000;
Integer timeout;
String username;
String password;
Integer port;
ProxyOptions preProxyOptions;
public HttpProxyConf() {
this.username = DEFAULT_USERNAME;
this.password = DEFAULT_PASSWORD;
this.timeout = DEFAULT_PORT;
this.timeout = DEFAULT_TIMEOUT;
this.preProxyOptions = new ProxyOptions();
}
public HttpProxyConf(JsonObject json) {
this();
}
public Integer getTimeout() {
return timeout;
}
public HttpProxyConf setTimeout(Integer timeout) {
this.timeout = timeout;
return this;
}
public String getUsername() {
return username;
}
public HttpProxyConf setUsername(String username) {
this.username = username;
return this;
}
public String getPassword() {
return password;
}
public HttpProxyConf setPassword(String password) {
this.password = password;
return this;
}
public Integer getPort() {
return port;
}
public HttpProxyConf setPort(Integer port) {
this.port = port;
return this;
}
public ProxyOptions getPreProxyOptions() {
return preProxyOptions;
}
public HttpProxyConf setPreProxyOptions(ProxyOptions preProxyOptions) {
this.preProxyOptions = preProxyOptions;
return this;
}
}

View File

@@ -1,2 +0,0 @@
app.version=${project.version}
build=${maven.build.timestamp}

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志自定义颜色 -->
<!-- https://logback.qos.ch/manual/layouts.html#coloring -->
<!--日志文件主目录:这里${user.home}为当前服务器用户主目录-->
<property name="LOG_HOME" value="logs"/>
<property name="LOGBACK_DEFAULT" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<property name="CUSTOMER_PATTERN2" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) -> %magenta([%15.15thread]) %cyan(%-40.40logger{39}) : %msg%n"/>
<!--配置日志文件(File)-->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--设置策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件路径:这里%d{yyyyMMdd}表示按天分类日志-->
<FileNamePattern>${LOG_HOME}/%d{yyyyMMdd}/run.log</FileNamePattern>
<!--日志保留天数-->
<MaxHistory>15</MaxHistory>
</rollingPolicy>
<!--设置格式-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度%msg日志消息%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<!-- 或者使用默认配置 -->
<!--<pattern>${FILE_LOG_PATTERN}</pattern>-->
<charset>utf8</charset>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 将文件输出设置成异步输出 -->
<appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="FILE"/>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度%msg日志消息%n是换行符-->
<pattern>${CUSTOMER_PATTERN2}</pattern>
</encoder>
</appender>
<logger name="io.netty" level="warn"/>
<logger name="io.vertx" level="info"/>
<root level="debug">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>

View File

@@ -20,55 +20,11 @@ Cloudreve自建网盘 (ce) {origin}/s/{shareKey}
缓存key -> 下载URL
分享链接 -> add 网盘类型 pwd origin(私有化) -> 直链
开源版 TODO
1. 缓存优化, 配置自动重载
2. 缓存删除接口(后台功能)
3. JS脚本引擎 自定义解析
专属版 功能设计
1. 支持绑定域名, 后台管理-账号管理, token管理, 账号解析次数限制
2. 流量统计, 文件分享信息, 目录解析, 文件云下载
3. IP代理池
网页跳转 防盗链
可禁用parser接口
标志短链 鉴权后 生成混淆链接
短链算法:
1. 基于Hash映射 hash(type:key:pwd) = h/xxxxx
鉴权实现:
auth-jdbc
// 基于标准SQL语法
支持H2, MySQL
用户:
jwt鉴权用户
角色:
超级管理员
注册用户
定义操作(权限):
用户的创建/删除/查询/修改, 生成短链/删除短链/修改解析次数和有效期/查询短链信息(
文件信息: 文件/文件夹, 文件数量, 文件大小, 文件类型; 链接信息: 解析次数, 缓存次数等)
微服务设计:
TODO
后台管理:
菜单:
网盘管理: token配置, 启用/禁用
短链管理: 短链列表, 新增, 删除
解析统计: 下载次数统计, 下载流量统计, 详细解析列表
状态监视: 服务请求并发数; 来源IP列表: 拉黑, 限制次数; Nginx
系统配置: 管理员账户, 系统参数: 域名配置, 预览URL,
https://f.ws59.cn/f/e3peohu6192
短链接设计

View File

@@ -10,11 +10,6 @@
</parent>
<artifactId>parser</artifactId>
<packaging>jar</packaging>
<name>cn.qaiu:parser</name>
<description>NFD parser</description>
<url>https://qaiu.top</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@@ -51,39 +46,10 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/license/mit</url>
</license>
</licenses>
<developers>
<developer>
<name>qaiu</name>
<email>qaiu00@gmail.com</email>
<organization>https://qaiu.top</organization>
</developer>
</developers>
<scm>
<connection>scm:git@github.com:qaiu/netdisk-fast-download.git</connection>
<developerConnection>scm:git@github.com:qaiu/netdisk-fast-download.git</developerConnection>
<url>git@github.com:qaiu/netdisk-fast-download.git</url>
</scm>
<distributionManagement>
<snapshotRepository>
<id>sonatype</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
</snapshotRepository>
<repository>
<id>sonatype</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<build>
<plugins>
@@ -96,49 +62,6 @@
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<attach>true</attach>
</configuration>
<executions>
<execution>
<id>attach-sources</id>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<!-- Gpg Signature -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.6.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,250 +0,0 @@
package cn.qaiu.entity;
import java.util.Map;
public class FileInfo {
/**
* 文件名
*/
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;
/**
* 扩展参数
*/
private Map<String, Object> extParameters;
public String getFileName() {
return fileName;
}
public FileInfo setFileName(String fileName) {
this.fileName = fileName;
return this;
}
public String getFileId() {
return fileId;
}
public FileInfo setFileId(String fileId) {
this.fileId = fileId;
return this;
}
public String getFileIcon() {
return fileIcon;
}
public FileInfo setFileIcon(String fileIcon) {
this.fileIcon = fileIcon;
return this;
}
public Long getSize() {
return size;
}
public FileInfo setSize(Long size) {
this.size = size;
return this;
}
public String getSizeStr() {
return sizeStr;
}
public FileInfo setSizeStr(String sizeStr) {
this.sizeStr = sizeStr;
return this;
}
public String getFileType() {
return fileType;
}
public FileInfo setFileType(String fileType) {
this.fileType = fileType;
return this;
}
public String getFilePath() {
return filePath;
}
public FileInfo setFilePath(String filePath) {
this.filePath = filePath;
return this;
}
public String getCreateTime() {
return createTime;
}
public FileInfo setCreateTime(String createTime) {
this.createTime = createTime;
return this;
}
public String getUpdateTime() {
return updateTime;
}
public FileInfo setUpdateTime(String updateTime) {
this.updateTime = updateTime;
return this;
}
public String getCreateBy() {
return createBy;
}
public FileInfo setCreateBy(String createBy) {
this.createBy = createBy;
return this;
}
public String getDescription() {
return description;
}
public FileInfo setDescription(String description) {
this.description = description;
return this;
}
public Integer getDownloadCount() {
return downloadCount;
}
public FileInfo setDownloadCount(Integer downloadCount) {
this.downloadCount = downloadCount;
return this;
}
public String getPanType() {
return panType;
}
public FileInfo setPanType(String panType) {
this.panType = panType;
return this;
}
public String getParserUrl() {
return parserUrl;
}
public FileInfo setParserUrl(String parserUrl) {
this.parserUrl = parserUrl;
return this;
}
public String getPreviewUrl() {
return previewUrl;
}
public FileInfo setPreviewUrl(String previewUrl) {
this.previewUrl = previewUrl;
return this;
}
public String getHash() {
return hash;
}
public FileInfo setHash(String hash) {
this.hash = hash;
return this;
}
public Map<String, Object> getExtParameters() {
return extParameters;
}
public FileInfo setExtParameters(Map<String, Object> extParameters) {
this.extParameters = extParameters;
return this;
}
@Override
public String toString() {
return "FileInfo{" +
"fileName='" + fileName + '\'' +
", fileId='" + fileId + '\'' +
", size=" + size +
", fileType='" + fileType + '\'' +
", filePath='" + filePath + '\'' +
", createTime='" + createTime + '\'' +
", updateTime='" + updateTime + '\'' +
", createBy='" + createBy + '\'' +
", description='" + description + '\'' +
", downloadCount=" + downloadCount +
", extParameters=" + extParameters +
'}';
}
}

View File

@@ -1,170 +0,0 @@
package cn.qaiu.entity;
import java.util.HashMap;
import java.util.Map;
public class ShareLinkInfo {
private String shareKey; // 分享键
private String panName; // 网盘名称
private String type; // 分享类型
private String sharePassword; // 分享密码(如果存在)
private String shareUrl; // 原始分享链接
private String standardUrl; // 规范化的标准链接
/**
* 其他参数预定义
* dirId: 目录ID 传入
* auths: 认证相关 传入
* UA: 浏览器请求头 传入
* fileInfo: 解析成功的文件信息对象 传出
*/
private Map<String, Object> otherParam;
private ShareLinkInfo(Builder builder) {
this.shareKey = builder.shareKey;
this.panName = builder.panName;
this.type = builder.type;
this.sharePassword = builder.sharePassword;
this.shareUrl = builder.shareUrl;
this.standardUrl = builder.standardUrl;
this.otherParam = builder.otherParam;
}
// Getter和Setter方法
public String getShareKey() {
return shareKey;
}
public String getPanName() {
return panName;
}
public void setShareKey(String shareKey) {
this.shareKey = shareKey;
}
public void setPanName(String panName) {
this.panName = panName;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getSharePassword() {
return sharePassword;
}
public void setSharePassword(String sharePassword) {
this.sharePassword = sharePassword;
}
public String getShareUrl() {
return shareUrl;
}
public void setShareUrl(String shareUrl) {
this.shareUrl = shareUrl;
}
public String getStandardUrl() {
return standardUrl;
}
public void setStandardUrl(String standardUrl) {
this.standardUrl = standardUrl;
}
public String getCacheKey() {
// 将type和shareKey组合成一个字符串作为缓存key
String key = type + ":" + shareKey;
if (type.equals("p115")) {
key += ("_" + otherParam.get("UA").toString().hashCode());
}
return key;
}
public ShareLinkInfo setOtherParam(Map<String, Object> otherParam) {
this.otherParam = otherParam;
return this;
}
public Map<String, Object> getOtherParam() {
return otherParam;
}
// 静态方法创建建造者对象
public static ShareLinkInfo.Builder newBuilder() {
return new ShareLinkInfo.Builder();
}
// 建造者类
public static class Builder {
public String panName; // 分享网盘名称
private String shareKey; // 分享键
private String type; // 分享类型 (网盘模板枚举的小写)
private String sharePassword = ""; // 分享密码(如果存在)
private String shareUrl; // 原始分享链接
private String standardUrl; // 规范化的标准链接
private Map<String, Object> otherParam = new HashMap<>(); // 其他参数
public Builder shareKey(String shareKey) {
this.shareKey = shareKey;
return this;
}
public Builder panName(String panName) {
this.panName = panName;
return this;
}
public Builder type(String type) {
this.type = type;
return this;
}
public Builder sharePassword(String sharePassword) {
this.sharePassword = sharePassword;
return this;
}
public Builder shareUrl(String shareUrl) {
this.shareUrl = shareUrl;
return this;
}
public Builder standardUrl(String standardUrl) {
this.standardUrl = standardUrl;
return this;
}
public Builder otherParam(Map<String, Object> otherParam) {
this.otherParam = otherParam;
return this;
}
public ShareLinkInfo build() {
return new ShareLinkInfo(this);
}
}
@Override
public String toString() {
return "ShareLinkInfo{" +
"shareKey='" + shareKey + '\'' +
", panName='" + panName + '\'' +
", type='" + type + '\'' +
", sharePassword='" + sharePassword + '\'' +
", shareUrl='" + shareUrl + '\'' +
", standardUrl='" + standardUrl + '\'' +
", otherParam='" + otherParam + '\'' +
'}';
}
}

View File

@@ -1,35 +1,60 @@
package cn.qaiu.parser;//package cn.qaiu.lz.common.parser;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.parser.impl.*;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import java.util.List;
public interface IPanTool {
Future<String> parse();
default String parseSync() {
return parse().toCompletionStage().toCompletableFuture().join();
static IPanTool typeMatching(String type, String key, String pwd) {
switch (type) {
case "lz": return new LzTool(key, pwd);
case "cow": return new CowTool(key, pwd);
case "ec": return new EcTool(key, pwd);
case "fc": return new FcTool(key, pwd);
case "uc": return new UcTool(key, pwd);
case "ye": return new YeTool(key, pwd);
case "fj": return new FjTool(key, pwd);
case "qk": return new QkTool(key, pwd);
case "le": return new LeTool(key, pwd);
case "ws": return new WsTool(key, pwd);
case "qq": return new QQTool(key, pwd);
case "iz": return new IzTool(key, pwd);
case "ce": return new CeTool(key, pwd);
default: throw new UnsupportedOperationException("未知分享类型");
}
}
/**
* 解析文件列表
* @return List
*/
default Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
promise.fail("Not implemented yet");
return promise.future();
static IPanTool shareURLPrefixMatching(String url, String pwd) {
if (url.contains(CowTool.LINK_KEY)) {
return new CowTool(url, pwd);
} else if (url.startsWith(EcTool.SHARE_URL_PREFIX)) {
return new EcTool(url, pwd);
} else if (url.startsWith(FcTool.SHARE_URL_PREFIX0)) {
return new FcTool(url, pwd);
} else if (url.startsWith(UcTool.SHARE_URL_PREFIX)) {
return new UcTool(url, pwd);
} else if (url.startsWith(YeTool.SHARE_URL_PREFIX)) {
return new YeTool(url, pwd);
} else if (url.startsWith(FjTool.SHARE_URL_PREFIX) || url.startsWith(FjTool.SHARE_URL_PREFIX2)) {
return new FjTool(url, pwd);
} else if (url.startsWith(IzTool.SHARE_URL_PREFIX)) {
return new IzTool(url, pwd);
} else if (url.contains(LzTool.LINK_KEY)) {
return new LzTool(url, pwd);
} else if (url.startsWith(LeTool.SHARE_URL_PREFIX)) {
return new LeTool(url, pwd);
} else if (url.contains(WsTool.SHARE_URL_PREFIX) || url.contains(WsTool.SHARE_URL_PREFIX2)) {
return new WsTool(url, pwd);
} else if (url.contains(QQTool.SHARE_URL_PREFIX)) {
return new QQTool(url, pwd);
} else if (url.contains("/s/")) {
// Cloudreve 网盘通用解析
return new CeTool(url, pwd);
}
throw new UnsupportedOperationException("未知分享类型");
}
/**
* 根据文件ID获取下载链接
* @return url
*/
default Future<String> parseById() {
Promise<String> promise = Promise.promise();
promise.complete("Not implemented yet");
return promise.future();
}
}

View File

@@ -1,39 +1,20 @@
package cn.qaiu.parser;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.entity.ShareLinkInfo;
import io.vertx.core.Future;
import cn.qaiu.util.CommonUtils;
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.ProxyOptions;
import io.vertx.core.net.ProxyType;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.ext.web.client.WebClientSession;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Iterator;
import java.util.zip.GZIPInputStream;
/**
* 解析器抽象类包含promise, HTTP Client, 默认失败方法等;
* 新增网盘解析器需要继承该类. <br>
* <h2>实现类命名规则: </h2>
* <p>{网盘标识}Tool, 网盘标识不超过5个字符, 可以取网盘名称首字母缩写或拼音首字母, <br>
* 音乐类型的解析以M开头, 例如网易云音乐Mne</p>
* 新增网盘解析器需要继承该类.
*/
public abstract class PanBase implements IPanTool {
public abstract class PanBase {
protected Logger log = LoggerFactory.getLogger(this.getClass());
protected Promise<String> promise = Promise.promise();
@@ -41,8 +22,7 @@ public abstract class PanBase implements IPanTool {
/**
* Http client
*/
protected WebClient client = WebClient.create(WebClientVertxInit.get(),
new WebClientOptions());
protected WebClient client = WebClient.create(WebClientVertxInit.get());
/**
* Http client session (会话管理, 带cookie请求)
@@ -55,7 +35,16 @@ public abstract class PanBase implements IPanTool {
protected WebClient clientNoRedirects = WebClient.create(WebClientVertxInit.get(),
new WebClientOptions().setFollowRedirects(false));
protected ShareLinkInfo shareLinkInfo;
/**
* 分享key 可以是整个URL; 如果是URL实现该类时要
* 使用{@link CommonUtils#adaptShortPaths(String urlPrefix, String key)}获取真实的分享key
*/
protected String key;
/**
* 分享密码
*/
protected String pwd;
/**
* 子类重写此构造方法不需要添加额外逻辑
@@ -66,47 +55,14 @@ public abstract class PanBase implements IPanTool {
* }
* </pre></blockquote>
*
* @param shareLinkInfo 分享链接信息
* @param key 分享key/url
* @param pwd 分享密码
*/
public PanBase(ShareLinkInfo shareLinkInfo) {
this.shareLinkInfo = shareLinkInfo;
if (shareLinkInfo.getOtherParam().containsKey("proxy")) {
JsonObject proxy = (JsonObject) shareLinkInfo.getOtherParam().get("proxy");
ProxyOptions proxyOptions = new ProxyOptions()
.setType(ProxyType.valueOf(proxy.getString("type").toUpperCase()))
.setHost(proxy.getString("host"))
.setPort(proxy.getInteger("port"));
if (StringUtils.isNotEmpty(proxy.getString("username"))) {
proxyOptions.setUsername(proxy.getString("username"));
}
if (StringUtils.isNotEmpty(proxy.getString("password"))) {
proxyOptions.setPassword(proxy.getString("password"));
}
this.client = WebClient.create(WebClientVertxInit.get(),
new WebClientOptions()
.setUserAgentEnabled(false)
.setProxyOptions(proxyOptions));
this.clientSession = WebClientSession.create(client);
this.clientNoRedirects = WebClient.create(WebClientVertxInit.get(),
new WebClientOptions().setFollowRedirects(false)
.setUserAgentEnabled(false)
.setProxyOptions(proxyOptions));
}
protected PanBase(String key, String pwd) {
this.key = key;
this.pwd = pwd;
}
protected PanBase() {
}
protected String baseMsg() {
if (shareLinkInfo.getShareUrl() != null) {
return shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": url=" + shareLinkInfo.getShareUrl();
}
return shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": key=" + shareLinkInfo.getShareKey() +
";pwd=" + shareLinkInfo.getSharePassword();
}
/**
* 失败时生成异常消息
*
@@ -116,22 +72,13 @@ public abstract class PanBase implements IPanTool {
*/
protected void fail(Throwable t, String errorMsg, Object... args) {
try {
// 判断是否已经完成
if (promise.future().isComplete()) {
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
return;
}
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
log.error("解析异常: " + s, t.fillInStackTrace());
promise.fail(baseMsg() + ": 解析异常: " + s + " -> " + t);
promise.fail(this.getClass().getSimpleName() + ": 解析异常: " + s + " -> " + t);
} catch (Exception e) {
log.error("ErrorMsg format fail. The parameter has been discarded", e);
log.error("解析异常: " + errorMsg, t.fillInStackTrace());
if (promise.future().isComplete()) {
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
return;
}
promise.fail(baseMsg() + ": 解析异常: " + errorMsg + " -> " + t);
promise.fail(this.getClass().getSimpleName() + ": 解析异常: " + errorMsg + " -> " + t);
}
}
@@ -143,27 +90,16 @@ public abstract class PanBase implements IPanTool {
*/
protected void fail(String errorMsg, Object... args) {
try {
// 判断是否已经完成
if (promise.future().isComplete()) {
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
return;
}
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
promise.fail(baseMsg() + " - 解析异常: " + s);
log.error("解析异常: " + s);
promise.fail(this.getClass().getSimpleName() + " - 解析异常: " + s);
} catch (Exception e) {
if (promise.future().isComplete()) {
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
return;
}
log.error("ErrorMsg format fail. The parameter has been discarded", e);
promise.fail(baseMsg() + " - 解析异常: " + errorMsg);
log.error("解析异常: " + errorMsg);
promise.fail(this.getClass().getSimpleName() + " - 解析异常: " + errorMsg);
}
}
protected void fail() {
fail("");
}
/**
* 生成失败Future的处理器
*
@@ -171,133 +107,7 @@ public abstract class PanBase implements IPanTool {
* @return Handler
*/
protected Handler<Throwable> handleFail(String errorMsg) {
return t -> fail(baseMsg() + " - 请求异常 {}: -> {}", errorMsg, t.fillInStackTrace());
return t -> fail(this.getClass().getSimpleName() + " - 请求异常 {}: -> {}", errorMsg, t.fillInStackTrace());
}
protected Handler<Throwable> handleFail() {
return handleFail("");
}
/**
* bodyAsJsonObject的封装, 会自动处理异常
*
* @param res HttpResponse
* @return JsonObject
*/
protected JsonObject asJson(HttpResponse<?> res) {
// 检查响应头中的Content-Encoding是否为gzip
String contentEncoding = res.getHeader("Content-Encoding");
try {
if ("gzip".equalsIgnoreCase(contentEncoding)) {
// 如果是gzip压缩的响应体解压
return new JsonObject(decompressGzip((Buffer) res.body()));
} else {
return res.bodyAsJsonObject();
}
} catch (Exception e) {
if ("gzip".equalsIgnoreCase(contentEncoding)) {
// 如果是gzip压缩的响应体解压
try {
log.error(decompressGzip((Buffer) res.body()));
fail(decompressGzip((Buffer) res.body()));
//throw new RuntimeException("响应不是JSON格式");
} catch (IOException ex) {
log.error("响应gzip解压失败");
fail("响应gzip解压失败: {}", ex.getMessage());
//throw new RuntimeException("响应gzip解压失败", ex);
}
} else {
log.error("解析失败: json格式异常: {}", res.bodyAsString());
fail("解析失败: json格式异常: {}", res.bodyAsString());
//throw new RuntimeException("解析失败: json格式异常");
}
return JsonObject.of();
}
}
/**
* body To text的封装, 会自动处理异常, 会自动解压gzip
* @param res HttpResponse
* @return String
*/
protected String asText(HttpResponse<?> res) {
// 检查响应头中的Content-Encoding是否为gzip
String contentEncoding = res.getHeader("Content-Encoding");
try {
if ("gzip".equalsIgnoreCase(contentEncoding)) {
// 如果是gzip压缩的响应体解压
return decompressGzip((Buffer) res.body());
} else {
return res.bodyAsString();
}
} catch (Exception e) {
fail("解析失败: res格式异常");
//throw new RuntimeException("解析失败: res格式异常");
}
return null;
}
protected void complete(String url) {
promise.complete(url);
}
protected Future<String> future() {
return promise.future();
}
/**
* 调用下一个解析器, 通用域名解析
*/
protected void nextParser() {
Iterator<PanDomainTemplate> iterator = Arrays.asList(PanDomainTemplate.values()).iterator();
while (iterator.hasNext()) {
if (iterator.next().name().equalsIgnoreCase(shareLinkInfo.getType())) {
if (iterator.hasNext()) {
PanDomainTemplate next = iterator.next();
log.debug("规则不匹配, 处理解析器转发: {} -> {}", shareLinkInfo.getPanName(), next.getDisplayName());
ParserCreate.fromType(next.name())
.fromAnyShareUrl(shareLinkInfo.getShareUrl())
.createTool()
.parse()
.onComplete(promise);
} else {
fail("error: 没有下一个解析处理器");
}
}
}
}
/**
* 解压gzip数据
* @param compressedData compressedData
* @return String
* @throws IOException IOException
*/
private String decompressGzip(Buffer compressedData) throws IOException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(compressedData.getBytes());
GZIPInputStream gzis = new GZIPInputStream(bais);
BufferedReader reader = new BufferedReader(new InputStreamReader(gzis,
StandardCharsets.UTF_8))) {
// 用于存储解压后的字符串
StringBuilder decompressedData = new StringBuilder();
// 逐行读取解压后的数据
String line;
while ((line = reader.readLine()) != null) {
decompressedData.append(line);
}
// 此时decompressedData.toString()包含了解压后的字符串
return decompressedData.toString();
}
}
protected String getDomainName(){
return shareLinkInfo.getOtherParam().getOrDefault("domainName", "").toString();
}
}

View File

@@ -1,413 +0,0 @@
package cn.qaiu.parser;
import cn.qaiu.parser.impl.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Pattern;
import static java.util.regex.Pattern.compile;
/**
* 枚举类 PanDomainTemplate 定义了不同网盘服务的模板信息,包括:
* <ul>
* <li>displayName: 网盘服务的显示名称,用于用户界面展示。</li>
* <li>regexPattern: 用于匹配和解析分享链接的正则表达式。</li>
* <li>standardUrlTemplate: 网盘服务的标准URL模板用于规范化分享链接。</li>
* <li>toolClass: 网盘解析工具实现类。</li>
* </ul>
* 请注意增添网盘时保证正则表达式最后一个捕捉组能匹配到分享key
* @author <a href="https://qaiu.top">QAIU</a>
* at 2023/6/13 4:26
*/
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-]+\\.)?(" +
"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),
// https://www.feijix.com/s/
// https://share.feijipan.com/s/
FJ("小飞机网盘",
compile("https://(share\\.feijipan\\.com|www\\.feijix\\.com)/s/(?<KEY>.+)"),
"https://www.feijix.com/s/{shareKey}",
FjTool.class),
// https://lecloud.lenovo.com/share/
LE("联想乐云",
compile("https://lecloud?\\.lenovo\\.com/share/(?<KEY>.+)"),
"https://lecloud.lenovo.com/share/{shareKey}",
LeTool.class),
// https://v2.fangcloud.com/s/
FC("亿方云",
compile("https://v2\\.fangcloud\\.(com|cn)/(s|sharing)/(?<KEY>.+)"),
"https://v2.fangcloud.com/s/{shareKey}",
"https://www.fangcloud.com/",
FcTool.class),
// https://wx.mail.qq.com/ftn/download?
QQ("QQ邮箱中转站",
compile("https://i?wx\\.mail\\.qq\\.com/ftn/download\\?(?<KEY>.+)"),
"https://iwx.mail.qq.com/ftn/download/{shareKey}",
"https://mail.qq.com",
QQTool.class),
// https://wx.mail.qq.com/s?k=uAG9JR42Rqgt010mFp
QQW("QQ邮箱云盘",
compile("https://i?wx\\.mail\\.qq\\.com/s\\?k=(?<KEY>.+)"),
"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\\.(" +
"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
EC("移动云空间",
compile("https://www\\.ecpan\\.cn/web(/%23|/#)?/yunpanProxy\\?path=.*&data=" +
"(?<KEY>[^&]+)&isShare=1"),
"https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={shareKey}&isShare=1",
EcTool.class),
// https://cowtransfer.com/s/
COW("奶牛快传",
compile("https://(.*)cowtransfer\\.com/s/(?<KEY>.+)"),
"https://cowtransfer.com/s/{shareKey}",
CowTool.class),
CT("城通网盘",
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" +
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"),
"https://474b.com/file/{shareKey}",
CtTool.class),
// https://xxx.118pan.com/bxxx
P118("118网盘",
compile("https://(?:[a-zA-Z\\d-]+\\.)?118pan\\.com/b(?<KEY>.+)"),
"https://qaiu.118pan.com/b{shareKey}",
P118Tool.class),
// https://www.vyuyun.com/s/QMa6ie?password=I4KG7H
// https://www.vyuyun.com/s/QMa6ie/file?password=I4KG7H
PVYY("微雨云存储",
compile("https://www\\.vyuyun\\.com/s/(?<KEY>[a-zA-Z\\d-]+)(/file)?(\\?password=(?<PWD>\\w+))?"),
"https://www.vyuyun.com/s/{shareKey}?password={pwd}",
PvyyTool.class),
// https://1drv.ms/w/s!Alg0feQmCv2rnRFd60DQOmMa-Oh_?e=buaRtp
// https://1drv.ms/u/c/abfd0a26e47d3458/EdYACWvPq85Et797YmvL5LgBruUKoNxqIFATXhIv1PI2_Q?e=z4ffNJ
POD("OneDrive",
compile("https://1drv\\.ms/(?<KEY>.+)"),
"https://1drv\\.ms/{shareKey}",
"https://onedrive.live.com/",
PodTool.class),
// 404网盘 https://drive.google.com/file/d/xxx/view?usp=sharing
PGD("GoogleDrive",
compile("https://drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?"),
"https://drive.google.com/file/d/{shareKey}/view?usp=sharing",
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-=]+)(#(.+))?"),
"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
PDB("dropbox",
compile("https://www.dropbox.com/scl/fi/(?<KEY>\\w+)/.+?rlkey=(?<PWD>\\w+).*"),
"https://www.dropbox.com/scl/fi/{shareKey}/?rlkey={pwd}&dl=0",
PdbTool.class),
P115("115网盘",
compile("https://(115|anxia).com/s/(?<KEY>\\w+)(\\?password=(?<PWD>\\w+))?([&#].*)?"),
"https://115.com/s/{shareKey}?password={pwd}",
P115Tool.class),
// 链接https://www.yunpan.com/surl_yD7wz4VgU9v提取码fc70
// P360("360云盘(需要referer头)",
// compile("https://www\\.yunpan\\.com/(?<KEY>\\w+)"),
// "https://www.yunpan.com/{shareKey}",
// P360Tool.class),
// https://pan-yz.cldisk.com/external/m/file/953658049102462976
Pcx("超星云盘(需要referer头)",
compile("https://pan-yz\\.cldisk\\.com/external/m/file/(?<KEY>\\w+)"),
"https://pan-yz.cldisk.com/external/m/file/{shareKey}",
PcxTool.class),
// =====================音乐类解析 分享链接标志->MxxS (单歌曲/普通音质)==========================
// http://163cn.tv/xxx
MNES("网易云音乐分享",
compile("http(s)?://163cn\\.tv/(?<KEY>.+)"),
"https://163cn.tv/{shareKey}",
MnesTool.class),
// https://music.163.com/#/song?id=xxx
MNE("网易云音乐歌曲详情",
compile("https://(y.)?music\\.163\\.com/(#|m/)?song\\?id=(?<KEY>.+)(&.*)?"),
"https://music.163.com/#/song?id={shareKey}",
MnesTool.MneTool.class),
// https://c6.y.qq.com/base/fcgi-bin/u?__=xxx
MQQS("QQ音乐分享",
compile("https://(?:[a-zA-Z\\d-]+\\.)?y\\.qq\\.com/base/fcgi-bin/u\\?__=(?<KEY>.+)"),
"https://c6.y.qq.com/base/fcgi-bin/u?__={shareKey}",
MqqsTool.class),
// https://y.qq.com/n/ryqq/songDetail/000XjcLg0fbRjv?songtype=0
MQQ("QQ音乐歌曲详情",
compile("https://y\\.qq\\.com/n/ryqq/songDetail/(?<KEY>.+)(\\?.*)?"),
"https://y.qq.com/n/ryqq/songDetail/{shareKey}",
MqqsTool.MqqTool.class),
// https://t1.kugou.com/song.html?id=xxx
MKGS("酷狗音乐分享",
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/song\\.html\\?id=(?<KEY>.+)"),
"https://t1.kugou.com/song.html?id={shareKey}",
MkgsTool.class),
// https://www.kugou.com/share/2bi8Fe9CSV3.html?id=2bi8Fe9CSV3#6ed9gna4"
MKGS2("酷狗音乐分享2",
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/share/(?<KEY>.+).html.*"),
"https://www.kugou.com/share/{shareKey}.html",
MkgsTool.Mkgs2Tool.class),
// https://www.kugou.com/mixsong/2bi8Fe9CSV3
MKG("酷狗音乐歌曲详情",
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/mixsong/(?<KEY>.+)\\.html.*"),
"https://www.kugou.com/mixsong/{shareKey}.html",
MkgsTool.MkgTool.class),
// https://kuwo.cn/play_detail/395500809
// https://m.kuwo.cn/newh5app/play_detail/318448522
MKWS("酷我音乐分享*",
compile("https://(m\\.)?kuwo\\.cn/(newh5app/)?play_detail/(?<KEY>.+)"),
"https://kuwo.cn/play_detail/{shareKey}",
MkwTool.class),
// https://music.migu.cn/v3/music/song/6326951FKBJ?channelId=001002H
MMGS("咪咕音乐分享",
compile("https://music\\.migu\\.cn/v3/music/song/(?<KEY>.+)(\\?.*)?"),
"https://music.migu.cn/v3/music/song/{shareKey}",
MmgTool.class),
// =====================私有盘解析==========================
// Cloudreve自定义域名解析, 解析器CeTool兜底策略, 即任意域名如果匹配不到对应的规则, 则由CeTool统一处理,
// 如果不属于Cloudreve盘 则调用下一个自定义域名解析器, 若都处理不了则抛出异常, 这种匹配模式类似责任链
// https://pan.huang1111.cn/s/xxx
// 通用域名([a-z\\d]+(-[a-z\\d]+)*\.)+[a-z]{2,}
CE("Cloudreve",
compile("http(s)?://([a-zA-Z\\d]+(-[a-zA-Z\\d]+)*\\.)+[a-zA-Z]{2,}(/s)?/(?<KEY>.+)"),
"https://{any}/s/{shareKey}",
"https://cloudreve.org/",
CeTool.class),
// 可道云自定义域名解析
KD("可道云",
compile("http(s)?://([a-zA-Z\\d]+(-[a-zA-Z\\d]+)*\\.)+[a-zA-Z]{2,}(/#s)?/(?<KEY>.+)"),
"https://{any}/#s/{shareKey}",
"https://kodcloud.com/",
KdTool.class),
// 其他自定义域名解析
OTHER("其他网盘",
compile("http(s)?://([a-zA-Z\\d]+(-[a-zA-Z\\d]+)*\\.)+[a-zA-Z]{2,}/(?<KEY>.+)"),
"https://{any}/{shareKey}",
OtherTool.class);
public static final String KEY = "KEY";
public static final String PWD = "PWD";
// 网盘的显示名称,用于用户界面显示
private final String displayName;
// 用于匹配和解析分享链接的正则表达式保证最后一个捕捉组能匹配到分享key
private final Pattern pattern;
private final String regex;
// 网盘的标准链接模板,不含占位符,用于规范化分享链接
private final String standardUrlTemplate;
// 网盘的域名, 如果在分享链接里能提取到, 则可不写
private String panDomain;
// 指向解析工具IPanTool实现类
private final Class<? extends IPanTool> toolClass;
PanDomainTemplate(String displayName, Pattern pattern, String standardUrlTemplate,
Class<? extends IPanTool> toolClass) {
this.displayName = displayName;
this.pattern = pattern;
this.regex = pattern.pattern();
this.standardUrlTemplate = standardUrlTemplate;
this.toolClass = toolClass;
}
PanDomainTemplate(String displayName, Pattern pattern, String standardUrlTemplate, String panDomain,
Class<? extends IPanTool> toolClass) {
this.displayName = displayName;
this.pattern = pattern;
this.regex = pattern.pattern();
this.standardUrlTemplate = standardUrlTemplate;
this.panDomain = panDomain;
this.toolClass = toolClass;
}
public String getDisplayName() {
return displayName;
}
public Pattern getPattern() {
return pattern;
}
public String getRegex() {
return regex;
}
public String getStandardUrlTemplate() {
return standardUrlTemplate;
}
public Class<? extends IPanTool> getToolClass() {
return toolClass;
}
public String getPanDomain() {
if (panDomain == null) {
String url = standardUrlTemplate
.replace("{shareKey}", "");
URL panDomainUrl = null;
try {
panDomainUrl = new URL(url);
} catch (MalformedURLException ignored) {}
return panDomainUrl != null ? (panDomainUrl.getProtocol() + "://" + panDomainUrl.getHost()) : "";
}
return panDomain;
}
}

View File

@@ -0,0 +1,13 @@
package cn.qaiu.parser;
/**
* @author <a href="https://qaiu.top">QAIU</a>
* @date 2023/6/13 4:26
*/
public enum PanType {
LZ("lz"),
COW("cow");
PanType(String type) {
}
}

View File

@@ -1,182 +0,0 @@
package cn.qaiu.parser;
import cn.qaiu.entity.ShareLinkInfo;
import org.apache.commons.lang3.StringUtils;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import static cn.qaiu.parser.PanDomainTemplate.KEY;
import static cn.qaiu.parser.PanDomainTemplate.PWD;
/**
* 该类提供方法来解析和规范化不同来源的分享链接,确保它们可以转换为统一的标准链接格式。
* 通过这种方式,应用程序可以更容易地处理和识别不同网盘服务的分享链接。
*
* @author <a href="https://qaiu.top">QAIU</a>
* @date 2024/9/15 14:10
*/
public class ParserCreate {
private final PanDomainTemplate panDomainTemplate;
private final ShareLinkInfo shareLinkInfo;
private String standardUrl;
public ParserCreate(PanDomainTemplate panDomainTemplate, ShareLinkInfo shareLinkInfo) {
this.panDomainTemplate = panDomainTemplate;
this.shareLinkInfo = shareLinkInfo;
this.standardUrl = panDomainTemplate.getStandardUrlTemplate();
}
// 解析并规范化分享链接
public ParserCreate normalizeShareLink() {
if (shareLinkInfo == null) {
throw new IllegalArgumentException("ShareLinkInfo not init");
}
// 匹配并提取shareKey
String shareUrl = shareLinkInfo.getShareUrl();
if (StringUtils.isEmpty(shareUrl)) {
throw new IllegalArgumentException("ShareLinkInfo shareUrl is empty");
}
Matcher matcher = this.panDomainTemplate.getPattern().matcher(shareUrl);
if (matcher.find()) {
String k0 = matcher.group(KEY);
String shareKey = URLEncoder.encode(k0, StandardCharsets.UTF_8);
// 返回规范化的标准链接
standardUrl = getStandardUrlTemplate()
.replace("{shareKey}", k0);
try {
String pwd = matcher.group(PWD);
if (StringUtils.isNotEmpty(pwd)) {
shareLinkInfo.setSharePassword(pwd);
}
standardUrl = standardUrl.replace("{pwd}", pwd);
} catch (Exception ignored) {}
shareLinkInfo.setShareUrl(shareUrl);
shareLinkInfo.setShareKey(shareKey);
if (!(panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal())) {
shareLinkInfo.setStandardUrl(standardUrl);
}
return this;
}
throw new IllegalArgumentException("Invalid share URL for " + this.panDomainTemplate.getDisplayName());
}
public IPanTool createTool() {
if (shareLinkInfo == null || StringUtils.isEmpty(shareLinkInfo.getType())) {
throw new IllegalArgumentException("ShareLinkInfo not init or type is empty");
}
if (StringUtils.isEmpty(shareLinkInfo.getShareKey())) {
this.normalizeShareLink();
}
try {
return this.panDomainTemplate.getToolClass()
.getDeclaredConstructor(ShareLinkInfo.class)
.newInstance(shareLinkInfo);
} catch (Exception e) {
throw new RuntimeException("无法创建工具实例: " + panDomainTemplate.getToolClass().getName(), e);
}
}
// set share key
public ParserCreate shareKey(String shareKey) {
if (panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal()) {
// 处理Cloudreve(ce)类: pan.huang1111.cn_s_wDz5TK _ -> /
String[] s = shareKey.split("_");
String standardUrl = "https://" + String.join("/", s);
shareLinkInfo.setShareKey(s[s.length - 1]);
shareLinkInfo.setStandardUrl(standardUrl);
shareLinkInfo.setShareUrl(standardUrl);
} else {
shareLinkInfo.setShareKey(shareKey);
standardUrl = standardUrl.replace("{shareKey}", shareKey);
shareLinkInfo.setStandardUrl(standardUrl);
}
if (StringUtils.isEmpty(shareLinkInfo.getShareUrl())) {
shareLinkInfo.setShareUrl(standardUrl);
}
return this;
}
// set any share url
public ParserCreate fromAnyShareUrl(String url) {
shareLinkInfo.setStandardUrl(url);
shareLinkInfo.setShareUrl(url);
return this;
}
public String getStandardUrlTemplate() {
return this.panDomainTemplate.getStandardUrlTemplate();
}
public ShareLinkInfo getShareLinkInfo() {
return shareLinkInfo;
}
public ParserCreate setShareLinkInfoPwd(String pwd) {
if (pwd != null) {
shareLinkInfo.setSharePassword(pwd);
standardUrl = standardUrl.replace("{pwd}", pwd);
shareLinkInfo.setStandardUrl(standardUrl);
if (shareLinkInfo.getShareUrl().contains("{pwd}")) {
shareLinkInfo.setShareUrl(standardUrl);
}
}
return this;
}
// 根据分享链接获取PanDomainTemplate实例
public synchronized static ParserCreate fromShareUrl(String shareUrl) {
for (PanDomainTemplate panDomainTemplate : PanDomainTemplate.values()) {
if (panDomainTemplate.getPattern().matcher(shareUrl).matches()) {
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
.type(panDomainTemplate.name().toLowerCase())
.panName(panDomainTemplate.getDisplayName())
.shareUrl(shareUrl).build();
if (panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal()) {
shareLinkInfo.setStandardUrl(shareUrl);
}
ParserCreate parserCreate = new ParserCreate(panDomainTemplate, shareLinkInfo);
return parserCreate.normalizeShareLink();
}
}
throw new IllegalArgumentException("Unsupported share URL");
}
// 根据type获取枚举实例
public synchronized static ParserCreate fromType(String type) {
try {
PanDomainTemplate panDomainTemplate = Enum.valueOf(PanDomainTemplate.class, type.toUpperCase());
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
.type(type.toLowerCase()).build();
shareLinkInfo.setPanName(panDomainTemplate.getDisplayName());
return new ParserCreate(panDomainTemplate, shareLinkInfo);
} catch (IllegalArgumentException ignore) {
// 如果没有找到对应的枚举实例,抛出异常
throw new IllegalArgumentException("No enum constant for type name: " + type);
}
}
// 生成parser短链path(不包含domainName)
public String genPathSuffix() {
String path;
if (panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal()) {
// 处理Cloudreve(ce)类: pan.huang1111.cn_s_wDz5TK _ -> /
path = this.shareLinkInfo.getType() + "/"
+ this.shareLinkInfo.getShareUrl()
.substring("https://".length()).replace("/", "_");
} else {
path = this.shareLinkInfo.getType() + "/" + this.shareLinkInfo.getShareKey();
}
String sharePassword = this.shareLinkInfo.getSharePassword();
return path + (StringUtils.isBlank(sharePassword) ? "" : ("@" + sharePassword));
}
}

View File

@@ -1,84 +1,73 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.parser.PanDomainTemplate;
import cn.qaiu.parser.ParserCreate;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Iterator;
/**
* <a href="https://github.com/cloudreve/Cloudreve">Cloudreve自建网盘解析</a> <br>
* <a href="https://github.com/cloudreve/Cloudreve">Cloudreve网盘解析</a> <br>
* <a href="https://pan.xiaomuxi.cn">暮希云盘</a> <br>
* <a href="https://pan.huang1111.cn">huang1111</a> <br>
* <a href="https://pan.seeoss.com">看见存储</a> <br>
* <a href="https://dav.yiandrive.com">亿安云盘</a> <br>
*/
public class CeTool extends PanBase {
public class CeTool extends PanBase implements IPanTool {
private static final String DOWNLOAD_API_PATH = "/api/v3/share/download/";
// api/v3/share/info/g31PcQ?password=qaiu
private static final String SHARE_API_PATH = "/api/v3/share/info/";
public CeTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public CeTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
String key = shareLinkInfo.getShareKey();
String pwd = shareLinkInfo.getSharePassword();
// https://pan.huang1111.cn/s/wDz5TK
// https://pan.huang1111.cn/s/y12bI6 -> https://pan.huang1111
// .cn/api/v3/share/download/y12bI6?path=undefined%2Fundefined;
// 类型解析 -> /ce/pan.huang1111.cn_s_wDz5TK
// 类型解析 -> /ce/https_pan.huang1111.cn_s_wDz5TK
// parser接口 -> /parser?url=https://pan.huang1111.cn/s/wDz5TK
try {
// // 处理URL
URL url = new URL(shareLinkInfo.getShareUrl());
String downloadApiUrl = url.getProtocol() + "://" + url.getHost() + DOWNLOAD_API_PATH + key + "?path" +
if (key.startsWith("https_") || key.startsWith("http_")) {
key = key.replace("https_", "https://")
.replace("http_", "http://")
.replace("_", "/");
}
// 处理URL
URL url = new URL(key);
String path = url.getPath();
String shareKey = path.substring(3);
String downloadApiUrl = url.getProtocol() + "://" + url.getHost() + DOWNLOAD_API_PATH + shareKey + "?path" +
"=undefined/undefined;";
String shareApiUrl = url.getProtocol() + "://" + url.getHost() + SHARE_API_PATH + key;
String shareApiUrl = url.getProtocol() + "://" + url.getHost() + SHARE_API_PATH + shareKey;
// 设置cookie
HttpRequest<Buffer> httpRequest = clientSession.getAbs(shareApiUrl);
if (pwd != null) {
httpRequest.addQueryParam("password", pwd);
}
// 获取下载链接
httpRequest.send().onSuccess(res -> {
try {
if (res.statusCode() == 200 && res.bodyAsJsonObject().containsKey("code")) {
getDownURL(downloadApiUrl);
} else {
nextParser();
}
} catch (Exception e) {
nextParser();
}
}).onFailure(handleFail(shareApiUrl));
} catch (Exception e) {
httpRequest.send().onSuccess(res -> getDownURL(downloadApiUrl)).onFailure(handleFail(shareApiUrl));
} catch (MalformedURLException e) {
fail(e, "URL解析错误");
}
return promise.future();
}
private void getDownURL(String shareApiUrl) {
clientSession.putAbs(shareApiUrl).send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
private void getDownURL(String apiUrl) {
clientSession.putAbs(apiUrl).send().onSuccess(res -> {
JsonObject jsonObject = res.bodyAsJsonObject();
System.out.println(jsonObject.encodePrettily());
if (jsonObject.containsKey("code") && jsonObject.getInteger("code") == 0) {
promise.complete(jsonObject.getString("data"));
} else {
fail("JSON解析失败: {}", jsonObject.encodePrettily());
}
}).onFailure(handleFail(shareApiUrl));
}).onFailure(handleFail(apiUrl));
}
}

View File

@@ -1,7 +1,8 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import org.apache.commons.lang3.StringUtils;
@@ -12,20 +13,22 @@ import org.apache.commons.lang3.StringUtils;
* @author <a href="https://qaiu.top">QAIU</a>
* @date 2023/4/21 21:19
*/
public class CowTool extends PanBase {
public class CowTool extends PanBase implements IPanTool {
private static final String API_REQUEST_URL = "https://cowtransfer.com/core/api/transfer/share";
public static final String SHARE_URL_PREFIX = "https://cowtransfer.com/s/";
public CowTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public static final String LINK_KEY = "cowtransfer.com/s/";
public CowTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
final String key = shareLinkInfo.getShareKey();
key = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key);
String url = API_REQUEST_URL + "?uniqueUrl=" + key;
client.getAbs(url).send().onSuccess(res -> {
JsonObject resJson = asJson(res);
JsonObject resJson = res.bodyAsJsonObject();
if ("success".equals(resJson.getString("message")) && resJson.containsKey("data")) {
JsonObject dataJson = resJson.getJsonObject("data");
String guid = dataJson.getString("guid");
@@ -40,7 +43,7 @@ public class CowTool extends PanBase {
}
String url2 = url2Build.toString();
client.getAbs(url2).send().onSuccess(res2 -> {
JsonObject res2Json = asJson(res2);
JsonObject res2Json = res2.bodyAsJsonObject();
if ("success".equals(res2Json.getString("message")) && res2Json.containsKey("data")) {
JsonObject data2 = res2Json.getJsonObject("data");
String downloadUrl = data2.getString("downloadUrl");

View File

@@ -1,105 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.RandomStringGenerator;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.uritemplate.UriTemplate;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Pattern;
/**
* <a href="https://www.ctfile.com">诚通网盘</a>
*/
public class CtTool extends PanBase {
private static final String API_URL_PREFIX = "https://webapi.ctfile.com";
// https://webapi.ctfile.com/getfile.php?path=f&f=55050874-1246660795-6464f6&
// passcode=7548&token=30wiijxs1fzhb6brw0p9m6&r=0.5885881231735761&
// ref=&url=https%3A%2F%2F474b.com%2Ff%2F55050874-1246660795-6464f6%3Fp%3D7548
private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" +
"&f={shareKey}&passcode={pwd}&token={token}&r={rand}&ref=";
//https://webapi.ctfile.com/get_file_url.php?uid=55050874&fid=1246660795&folder_id=0&
// file_chk=054bc20461f5c63ff82015b9e69fb7fc&mb=1&token=30wiijxs1fzhb6brw0p9m6&app=0&
// acheck=1&verifycode=&rd=0.965929071503574
private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" +
"uid={uid}&fid={fid}&folder_id=0&file_chk={file_chk}&mb=0&token={token}&app=0&acheck=0&verifycode=" +
"&rd={rand}";
/**
* 子类重写此构造方法不需要添加额外逻辑
* 如:
* <blockquote><pre>
* public XxTool(String key, String pwd) {
* super(key, pwd);
* }
* </pre></blockquote>
*
* @param shareLinkInfo 分享链接信息
*/
public CtTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
@Override
public Future<String> parse() {
final String shareKey = shareLinkInfo.getShareKey();
if (shareKey.indexOf('-') == -1) {
fail("shareKey格式不正确找不到'-': {}", shareKey);
return promise.future();
}
String[] split = shareKey.split("-");
String uid = split[0], fid = split[1];
String token = RandomStringGenerator.generateRandomString();
// 获取url path
int i1 = shareLinkInfo.getShareUrl().indexOf("com/");
int i2 = shareLinkInfo.getShareUrl().lastIndexOf("/");
String path = shareLinkInfo.getShareUrl().substring(i1 + 4, i2);
HttpRequest<Buffer> bufferHttpRequest1 = clientSession.getAbs(UriTemplate.of(API1))
.setTemplateParam("path", path)
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
.setTemplateParam("token", token)
.setTemplateParam("r", Math.random() + "");
bufferHttpRequest1
.send().onSuccess(res -> {
var resJson = asJson(res);
if (resJson.containsKey("file")) {
var fileJson = resJson.getJsonObject("file");
if (fileJson.containsKey("file_chk")) {
var file_chk = fileJson.getString("file_chk");
HttpRequest<Buffer> bufferHttpRequest2 = clientSession.getAbs(UriTemplate.of(API2))
.setTemplateParam("uid", uid)
.setTemplateParam("fid", fid)
.setTemplateParam("file_chk", file_chk)
.setTemplateParam("token", token)
.setTemplateParam("rd", Math.random() + "");
bufferHttpRequest2
.send().onSuccess(res2 -> {
JsonObject resJson2 = asJson(res2);
if (resJson2.containsKey("downurl")) {
promise.complete(resJson2.getString("downurl"));
} else {
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson2, "downurl");
}
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
} else {
fail("解析失败, file_chk找不到, 可能分享已失效或者分享密码不对: {}", fileJson);
}
} else {
fail("解析失败, 文件信息为空, 可能分享已失效");
}
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
return promise.future();
}
}

View File

@@ -1,7 +1,8 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import io.vertx.core.Future;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
@@ -10,30 +11,28 @@ import io.vertx.uritemplate.UriTemplate;
/**
* 移动云空间解析
*/
public class EcTool extends PanBase {
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data=4b3d786755688b85c6eb0c04b9124f4dalzdaJpXHx&isShare=1
public class EcTool extends PanBase implements IPanTool {
private static final String FIRST_REQUEST_URL = "https://www.ecpan.cn/drive/fileextoverrid" +
".do?extractionCode={extractionCode}&chainUrlTemplate=https:%2F%2Fwww.ecpan" +
".cn%2Fweb%2F%23%2FyunpanProxy%3Fpath%3D%252F%2523%252Fdrive%252Foutside&parentId=-1&data={dataKey}";
private static final String DOWNLOAD_REQUEST_URL = "https://www.ecpan.cn/drive/sharedownload.do";
public EcTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public static final String SHARE_URL_PREFIX = "www.ecpan.cn/";
public EcTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
final String dataKey = shareLinkInfo.getShareKey();
final String pwd = shareLinkInfo.getSharePassword();
String dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key);
// 第一次请求 获取文件信息
client.getAbs(UriTemplate.of(FIRST_REQUEST_URL))
.setTemplateParam("dataKey", dataKey)
.setTemplateParam("extractionCode", pwd == null ? "" : pwd)
.send()
.onSuccess(res -> {
JsonObject jsonObject = asJson(res);
JsonObject jsonObject = res.bodyAsJsonObject();
log.debug("ecPan get file info -> {}", jsonObject);
JsonObject fileInfo = jsonObject
.getJsonObject("var")
@@ -58,7 +57,7 @@ public class EcTool extends PanBase {
// 第二次请求 获取下载链接
client.postAbs(DOWNLOAD_REQUEST_URL).sendJsonObject(requestBodyJson).onSuccess(res2 -> {
JsonObject jsonRes = asJson(res2);
JsonObject jsonRes = res2.bodyAsJsonObject();
log.debug("ecPan get download url -> {}", res2.body().toString());
promise.complete(jsonRes.getJsonObject("var").getString("downloadUrl"));
}).onFailure(handleFail(""));

View File

@@ -1,7 +1,8 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
@@ -18,21 +19,21 @@ import java.util.regex.Pattern;
/**
* 360亿方云
*/
public class FcTool extends PanBase {
public class FcTool extends PanBase implements IPanTool {
public static final String SHARE_URL_PREFIX0 = "https://v2.fangcloud.com/s";
public static final String SHARE_URL_PREFIX = "https://v2.fangcloud.com/sharing/";
public static final String SHARE_URL_PREFIX2 = "https://v2.fangcloud.cn/sharing/";
private static final String DOWN_REQUEST_URL = "https://v2.fangcloud.cn/apps/files/download?file_id={fid}" +
"&scenario=share&unique_name={uname}";
public FcTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public FcTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
final String dataKey = shareLinkInfo.getShareKey();
final String pwd = shareLinkInfo.getSharePassword();
String data = key.replace("share","sharing");
String dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, data);
WebClientSession sClient = WebClientSession.create(client);
// 第一次请求 自动重定向
sClient.getAbs(SHARE_URL_PREFIX + dataKey).send().onSuccess(res -> {
@@ -87,7 +88,7 @@ public class FcTool extends PanBase {
.setTemplateParam("unique_name", dataKey).send().onSuccess(res2 -> {
JsonObject resJson;
try {
resJson = asJson(res2);
resJson = res2.bodyAsJsonObject();
} catch (Exception e) {
fail(e, DOWN_REQUEST_URL + " 第二次请求没有返回JSON, 可能下载受限");
return;

View File

@@ -1,39 +1,33 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.AESUtils;
import cn.qaiu.util.FileSizeConverter;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.UUIDUtil;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
/**
* 小飞机网盘
*
* @version V016_230609
*/
public class FjTool extends PanBase {
public class FjTool extends PanBase implements IPanTool {
public static final String SHARE_URL_PREFIX = "https://www.feijix.com/s/";
public static final String REFERER_URL = "https://share.feijipan.com/";
public static final String SHARE_URL_PREFIX2 = REFERER_URL + "s/";
private static final String API_URL_PREFIX = "https://api.feijipan.com/ws/";
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
"&uuid={uuid}&extra=2&timestamp={ts}&shareId={shareId}&type=0&offset=1&limit=60";
/// recommend/list?devType=6&devModel=Chrome&uuid={uuid}&extra=2&timestamp={ts}&shareId={shareId}&type=0&offset=1&limit=60
// recommend/list?devType=6&devModel=Chrome&uuid={uuid}&extra=2&timestamp={ts}&shareId=JoUTkZYj&type=0&offset=1&limit=60
/// recommend/list?devType=6&devModel=Chrome&uuid={uuid}&extra=2&timestamp={ts}&shareId={shareId}&type=0&offset=1
// &limit=60
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
"&devType=6&uuid={uuid}&timestamp={ts}&auth={auth}&shareId={dataKey}";
@@ -44,94 +38,50 @@ public class FjTool extends PanBase {
"={uuid}&extra=2&timestamp={ts}";
// https://api.feijipan.com/ws/buy/vip/list?devType=6&devModel=Chrome&uuid=WQAl5yBy1naGudJEILBvE&extra=2&timestamp=E2C53155F6D09417A27981561134CB73
// https://api.feijipan.com/ws/share/list?devType=6&devModel=Chrome&uuid=pwRWqwbk1J-KMTlRZowrn&extra=2&timestamp=C5F8A68C53121AB21FA35BA3529E8758&shareId=fmAuOh3m&folderId=28986333&offset=1&limit=60
private static final String FILE_LIST_URL = API_URL_PREFIX + "/share/list?devType=6&devModel=Chrome&uuid" +
"={uuid}&extra=2&timestamp={ts}&shareId={shareId}&folderId" +
"={folderId}&offset=1&limit=60";
private static final MultiMap header;
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2Hex(Long.toString(nowTs));
String uuid = UUIDUtil.fjUuid(); // 也可以使用 UUID.randomUUID().toString()
static {
header = MultiMap.caseInsensitiveMultiMap();
header.set("Accept", "application/json, text/plain, */*");
header.set("Accept-Encoding", "gzip, deflate, br, zstd");
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
header.set("Content-Length", "0");
header.set("DNT", "1");
header.set("Host", "api.feijipan.com");
header.set("Origin", "https://www.feijix.com");
header.set("Pragma", "no-cache");
header.set("Referer", "https://www.feijix.com/");
header.set("Sec-Fetch-Dest", "empty");
header.set("Sec-Fetch-Mode", "cors");
header.set("Sec-Fetch-Site", "cross-site");
header.set("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");
header.set("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
header.set("sec-ch-ua-mobile", "?0");
header.set("sec-ch-ua-platform", "\"Windows\"");
}
public FjTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public FjTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
String dataKey;
if (key.startsWith(SHARE_URL_PREFIX2)) {
dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX2, key);
} else {
dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key);
}
// 240530 此处shareId又改为了原始的shareId
// String.valueOf(AESUtils.idEncrypt(dataKey));
final String shareId = shareLinkInfo.getShareKey();
// 240530 此处shareId又改为了原始的shareId, nm玩呢?
String shareId = dataKey; // String.valueOf(AESUtils.idEncrypt(dataKey));
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2Hex(Long.toString(nowTs));
String uuid = UUIDUtil.fjUuid(); // 也可以使用 UUID.randomUUID().toString()
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(r0 -> { // 忽略res
// long nowTs0 = System.currentTimeMillis();
String tsEncode0 = AESUtils.encrypt2Hex(Long.toString(nowTs));
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
client.postAbs(UriTemplate.of(url))
.putHeaders(header)
client.postAbs(UriTemplate.of(FIRST_REQUEST_URL))
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("ts", tsEncode0)
.send().onSuccess(res -> {
JsonObject resJson = asJson(res);
JsonObject resJson = res.bodyAsJsonObject();
if (resJson.getInteger("code") != 200) {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
if (resJson.getJsonArray("list").size() == 0) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
// 如果是目录返回目录ID
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
return;
}
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
if (fileList.getInteger("fileType") == 2) {
promise.complete(fileList.getInteger("folderId").toString());
return;
}
String fileId = fileInfo.getString("fileIds");
String userId = fileInfo.getString("userId");
// 其他参数
@@ -140,16 +90,18 @@ public class FjTool extends PanBase {
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
MultiMap headers0 = MultiMap.caseInsensitiveMultiMap();
headers0.set("referer", REFERER_URL);
// 第二次请求
HttpRequest<Buffer> httpRequest =
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.putHeaders(headers0)
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", shareId);
// System.out.println(httpRequest.toString());
.setTemplateParam("dataKey", dataKey);
System.out.println(httpRequest.toString());
httpRequest.send().onSuccess(res2 -> {
MultiMap headers = res2.headers();
if (!headers.contains("Location")) {
@@ -164,145 +116,4 @@ public class FjTool extends PanBase {
return promise.future();
}
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
parserDir(id, shareId, promise);
} else {
promise.fail("解析目录ID失败");
}
}).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes);
});
return promise.future();
}
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject;
try {
jsonObject = asJson(res);
} catch (Exception e) {
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
return;
}
// System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 映射已知字段fileInfo
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 其他参数
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2Hex(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileJson.getString("fileId"))
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
promise.complete(result);
}).onFailure(failRes -> {
log.error("解析目录请求失败: {}", failRes.getMessage());
promise.fail(failRes);
});;
}
@Override
public Future<String> parseById() {
// 第二次请求
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))
.setTemplateParam("ts", paramJson.getString("ts"))
.setTemplateParam("auth", paramJson.getString("auth"))
.setTemplateParam("dataKey", paramJson.getString("shareId"))
.putHeaders(header).send().onSuccess(res2 -> {
MultiMap headers = res2.headers();
if (!headers.contains("Location")) {
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res2.headers());
return;
}
promise.complete(headers.get("Location"));
}).onFailure(handleFail(SECOND_REQUEST_URL));
return promise.future();
}
}

View File

@@ -1,132 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.impl.headers.HeadersMultiMap;
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.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* t.cn 短链生成 //
*/
public class GenShortUrl extends PanBase {
private static final Logger log = LoggerFactory.getLogger(GenShortUrl.class);
private static final String COMMENT_URL = "https://www.weibo.com/aj/v6/comment/add";
private static final String DELETE_COMMENT_URL = "https://www.weibo.com/aj/comment/del";
private static final String WRAPPER_URL = "https://www.so.com/link?m=ewgUSYiFWXIoTybC3fJH8YoJy8y10iRquo6cazgINwWjTn3HvVJ92TrCJu0PmMUR0RMDfOAucP3wa4G8j64SrhNH9Z0Cr0PEyn9ASuvpkUGmAjjUEGJkO5%2BIDGWVrEkPHsL7UsoKO6%2BlT%2BD6r&ccc=";
private static final String MID = "5095144728824883"; // 微博的mid
private static final MultiMap HEADER = HeadersMultiMap.headers()
.add("Content-Type", "application/x-www-form-urlencoded")
.add("Referer", "https://www.weibo.com")
.add("Content-Type", "application/x-www-form-urlencoded")
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36");
Cookie cookie = new DefaultCookie("SUB", "_2A25KJE5vDeRhGeRJ6lsR9SjJzDuIHXVpWM-nrDV8PUJbkNAbLVPlkW1NUmJm3GjYtRHBsHdMUKafkdTL_YheMEmu");
public GenShortUrl(ShareLinkInfo build) {
super(build);
}
@Override
public Future<String> parse() {
String longUrl = shareLinkInfo.getShareUrl();
String url = WRAPPER_URL + Base64.getEncoder().encodeToString(longUrl.getBytes());
String payload = "mid=" + MID + "&content=" + URLEncoder.encode(url, StandardCharsets.UTF_8);
clientSession.cookieStore().put(cookie);
clientSession.postAbs(COMMENT_URL)
.putHeaders(HEADER)
.sendBuffer(Buffer.buffer(payload))
.onSuccess(res -> {
JsonObject data = asJson(res).getJsonObject("data");
if (data.isEmpty()) {
fail(asJson(res).getString("msg"));
return;
}
String comment = data.getString("comment");
String shortUrl = extractShortUrl(comment);
if (shortUrl != null) {
log.info("生成的短链:{}", shortUrl);
String commentId = extractCommentId(comment);
if (commentId != null) {
deleteComment(commentId);
} else {
promise.fail("未能提取评论ID");
}
} else {
promise.fail("未能生成短链");
}
})
.onFailure(err -> {
log.error("添加评论失败", err);
promise.fail(err);
});
return promise.future();
}
private void deleteComment(String commentId) {
String payload = "mid=" + MID + "&cid=" + commentId;
clientSession.postAbs(DELETE_COMMENT_URL)
.putHeaders(HEADER)
.sendBuffer(Buffer.buffer(payload))
.onSuccess(res -> {
JsonObject responseJson = res.bodyAsJsonObject();
if (responseJson.getString("code").equals("100000")) {
log.info("评论已删除: {}", commentId);
} else {
log.error("删除评论失败: {}", responseJson.encode());
}
})
.onFailure(err -> {
log.error("删除评论失败", err);
});
}
private String extractShortUrl(String comment) {
Pattern pattern = Pattern.compile("(https?)://t.cn/\\w+");
Matcher matcher = pattern.matcher(comment);
if (matcher.find()) {
return matcher.group(0);
}
return null;
}
private String extractCommentId(String comment) {
Pattern pattern = Pattern.compile("comment_id=\"(\\d+)\"");
Matcher matcher = pattern.matcher(comment);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
public static void main(String[] args) {
// http://t.cn/A6nfZn86
// http://t.cn/A6nfZn86
new GenShortUrl(ShareLinkInfo.newBuilder().shareUrl("https://qaiu.top/sdfsdf").build()).parse().onSuccess(
System.out::println
);
}
}

View File

@@ -1,283 +1,79 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.AESUtils;
import cn.qaiu.util.FileSizeConverter;
import cn.qaiu.util.UUIDUtil;
import cn.qaiu.util.CommonUtils;
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.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.UUID;
/**
* 蓝奏云优享
*
*/
public class IzTool extends PanBase {
public class IzTool extends PanBase implements IPanTool {
public static final String SHARE_URL_PREFIX = "https://www.ilanzou.com/s/";
private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/";
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
"&uuid={uuid}&extra=2&timestamp={ts}&shareId={shareId}&type=0&offset=1&limit=60";
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome&extra" +
"=2&shareId={shareId}&type=0&offset=1&limit=60";
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
"&devType=6&uuid={uuid}&timestamp={ts}&auth={auth}&shareId={dataKey}";
// downloadId=x&enable=1&devType=6&uuid=x&timestamp=x&auth=x&shareId=lGFndCM
"&devType=6&uuid={uuid}&timestamp={ts}&auth={auth}";
private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" +
"={uuid}&extra=2&timestamp={ts}";
private static final String FILE_LIST_URL = API_URL_PREFIX + "/share/list?devType=6&devModel=Chrome&uuid" +
"={uuid}&extra=2&timestamp={ts}&shareId={shareId}&folderId" +
"={folderId}&offset=1&limit=60";
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
String uuid = UUID.randomUUID().toString();
private static final MultiMap header;
static {
header = MultiMap.caseInsensitiveMultiMap();
header.set("Accept", "application/json, text/plain, */*");
header.set("Accept-Encoding", "gzip, deflate, br, zstd");
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
header.set("Content-Length", "0");
header.set("DNT", "1");
header.set("Host", "api.ilanzou.com");
header.set("Origin", "https://www.ilanzou.com/");
header.set("Pragma", "no-cache");
header.set("Referer", "https://www.ilanzou.com/");
header.set("Sec-Fetch-Dest", "empty");
header.set("Sec-Fetch-Mode", "cors");
header.set("Sec-Fetch-Site", "cross-site");
header.set("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");
header.set("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
header.set("sec-ch-ua-mobile", "?0");
header.set("sec-ch-ua-platform", "\"Windows\"");
}
public IzTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public IzTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
String shareId = shareLinkInfo.getShareKey();
String dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key);
// 24.5.12 ilanzou改规则无需计算shareId
// String shareId = String.valueOf(AESUtils.idEncryptIz(dataKey));
// 第一次请求 获取文件信息
// POST https://api.ilanzou.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(r0 -> { // 忽略res
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
client.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(res -> {
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 200) {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
// 如果是目录返回目录ID
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
return;
}
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
if (fileList.getInteger("fileType") == 2) {
promise.complete(fileList.getInteger("folderId").toString());
return;
}
String fileId = fileInfo.getString("fileIds");
String userId = fileInfo.getString("userId");
// 其他参数
// String fidEncode = AESUtils.encrypt2HexIz(fileId + "|");
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
// 第二次请求
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("auth", auth)
.setTemplateParam("shareId", shareId)
.putHeaders(header).send().onSuccess(res2 -> {
MultiMap headers = res2.headers();
if (!headers.contains("Location")) {
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + headers);
return;
}
promise.complete(headers.get("Location"));
}).onFailure(handleFail(SECOND_REQUEST_URL));
}).onFailure(handleFail(FIRST_REQUEST_URL));
});
return promise.future();
}
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
parserDir(id, shareId, promise);
} else {
promise.fail("解析目录ID失败");
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
client.postAbs(UriTemplate.of(FIRST_REQUEST_URL)).setTemplateParam("shareId", dataKey).send().onSuccess(res -> {
JsonObject resJson = res.bodyAsJsonObject();
if (resJson.getInteger("code") != 200) {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
}).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes);
});
return promise.future();
}
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject;
try {
jsonObject = asJson(res);
} catch (Exception e) {
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
return;
}
// System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
if (resJson.getJsonArray("list").size() == 0) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
String fileId = fileInfo.getString("fileIds");
String userId = fileInfo.getString("userId");
// 其他参数
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
String uuid = UUID.randomUUID().toString();
// String fidEncode = AESUtils.encrypt2HexIz(fileId + "|");
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
// 第二次请求
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("auth", auth).send().onSuccess(res2 -> {
MultiMap headers = res2.headers();
if (!headers.contains("Location")) {
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res.headers());
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
promise.complete(result);
}).onFailure(failRes -> {
log.error("解析目录请求失败: {}", failRes.getMessage());
promise.fail(failRes);
});
}
promise.complete(headers.get("Location"));
}).onFailure(handleFail(SECOND_REQUEST_URL));
}).onFailure(handleFail(FIRST_REQUEST_URL));
@Override
public Future<String> parseById() {
// 第二次请求
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))
.setTemplateParam("ts", paramJson.getString("ts"))
.setTemplateParam("auth", paramJson.getString("auth"))
.setTemplateParam("shareId", paramJson.getString("shareId"))
.putHeaders(header).send().onSuccess(res2 -> {
MultiMap headers = res2.headers();
if (!headers.contains("Location")) {
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res2.headers());
return;
}
promise.complete(headers.get("Location"));
}).onFailure(handleFail(SECOND_REQUEST_URL));
return promise.future();
}
}

View File

@@ -1,26 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import java.util.UUID;
/**
* <a href="https://kodcloud.com/">可道云</a>
*/
public class KdTool extends PanBase {
private static final String API_URL_PREFIX = "";
public KdTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
nextParser();
// TODO
return future();
}
}

View File

@@ -1,7 +1,8 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import io.vertx.core.Future;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
@@ -11,22 +12,23 @@ import java.util.UUID;
/**
* <a href="https://lecloud.lenovo.com/">联想乐云</a>
*/
public class LeTool extends PanBase {
public class LeTool extends PanBase implements IPanTool {
public static final String SHARE_URL_PREFIX = "https://lecloud.lenovo.com/share/";
private static final String API_URL_PREFIX = "https://lecloud.lenovo.com/share/api/clouddiskapi/share/public/v1/";
public LeTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public LeTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
final String dataKey = shareLinkInfo.getShareKey();
final String pwd = shareLinkInfo.getSharePassword();
String dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key);
// {"shareId":"xxx","password":"xxx","directoryId":"-1"}
String apiUrl1 = API_URL_PREFIX + "shareInfo";
client.postAbs(apiUrl1)
.sendJsonObject(JsonObject.of("shareId", dataKey, "password", pwd, "directoryId", -1))
.onSuccess(res -> {
JsonObject resJson = asJson(res);
JsonObject resJson = res.bodyAsJsonObject();
if (resJson.containsKey("result")) {
if (resJson.getBoolean("result")) {
JsonObject dataJson = resJson.getJsonObject("data");
@@ -45,7 +47,7 @@ public class LeTool extends PanBase {
JsonObject fileInfoJson = files.getJsonObject(0);
if (fileInfoJson != null) {
// TODO 文件大小fileSize和文件名fileName
String fileId = fileInfoJson.getString("fileId");
Long fileId = fileInfoJson.getLong("fileId");
// 根据文件ID获取跳转链接
getDownURL(dataKey, fileId);
}
@@ -59,7 +61,7 @@ public class LeTool extends PanBase {
return promise.future();
}
private void getDownURL(String key, String fileId) {
private void getDownURL(String key, Long fileId) {
String uuid = UUID.randomUUID().toString();
JsonArray fileIds = JsonArray.of(fileId);
String apiUrl2 = API_URL_PREFIX + "packageDownloadWithFileIds";
@@ -67,7 +69,7 @@ public class LeTool extends PanBase {
client.postAbs(apiUrl2)
.sendJsonObject(JsonObject.of("fileIds", fileIds, "shareId", key, "browserId", uuid))
.onSuccess(res -> {
JsonObject resJson = asJson(res);
JsonObject resJson = res.bodyAsJsonObject();
if (resJson.containsKey("result")) {
if (resJson.getBoolean("result")) {
JsonObject dataJson = resJson.getJsonObject("data");

View File

@@ -1,22 +1,15 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CastUtil;
import cn.qaiu.util.FileSizeConverter;
import cn.qaiu.util.HeaderUtils;
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.JsonObject;
import io.vertx.ext.web.client.WebClient;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import javax.script.ScriptException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -26,18 +19,19 @@ import java.util.regex.Pattern;
*
* @author QAIU
*/
public class LzTool extends PanBase {
public class LzTool extends PanBase implements IPanTool {
public static final String SHARE_URL_PREFIX = "https://wwww.lanzoup.com";
public static final String SHARE_URL_PREFIX = "https://wwwa.lanzoux.com";
public static final String LINK_KEY = "lanzou";
public LzTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public LzTool(String key, String pwd) {
super(key, pwd);
}
@SuppressWarnings("unchecked")
public Future<String> parse() {
String sUrl = shareLinkInfo.getStandardUrl();
String pwd = shareLinkInfo.getSharePassword();
String sUrl = key.startsWith("https://") ? key : SHARE_URL_PREFIX + "/" + key;
WebClient client = clientNoRedirects;
client.getAbs(sUrl).send().onSuccess(res -> {
@@ -47,52 +41,47 @@ public class LzTool extends PanBase {
Matcher matcher = compile.matcher(html);
// 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面
if (!matcher.find()) {
// 处理一下JS
String jsText = getJsText(html);
if (jsText == null) {
fail(SHARE_URL_PREFIX + " -> " + sUrl + ": js脚本匹配失败, 可能分享已失效");
return;
}
jsText = jsText.replace("document.getElementById('pwd').value", "\"" + pwd + "\"");
jsText = jsText.substring(0, jsText.indexOf("document.getElementById('rpt')"));
try {
String jsText = getJsByPwd(pwd, html, "document.getElementById('rpt')");
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "down_p");
getDownURL(sUrl, client, scriptObjectMirror);
} catch (Exception e) {
getDownURL(sUrl, client, (Map<String, String>) scriptObjectMirror.get("data"));
} catch (ScriptException | NoSuchMethodException e) {
fail(e, "js引擎执行失败");
return;
}
return;
}
String iframePath = matcher.group(1);
client.getAbs(SHARE_URL_PREFIX + iframePath).send().onSuccess(res2 -> {
String html2 = res2.bodyAsString();
// 去TMD正则
// Matcher matcher2 = Pattern.compile("'sign'\s*:\s*'(\\w+)'").matcher(html2);
String jsText = getJsText(html2);
if (jsText == null) {
fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": js脚本匹配失败, 可能分享已失效");
return;
}
try {
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null);
getDownURL(sUrl, client, (Map<String, String>) scriptObjectMirror.get("data"));
} catch (ScriptException | NoSuchMethodException e) {
fail(e, "js引擎执行失败");
}
} else {
// 没有密码
String iframePath = matcher.group(1);
client.getAbs(SHARE_URL_PREFIX + iframePath).send().onSuccess(res2 -> {
String html2 = res2.bodyAsString();
// 去TMD正则
// Matcher matcher2 = Pattern.compile("'sign'\s*:\s*'(\\w+)'").matcher(html2);
String jsText = getJsText(html2);
if (jsText == null) {
fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": js脚本匹配失败, 可能分享已失效");
return;
}
try {
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null);
getDownURL(sUrl, client, scriptObjectMirror);
} catch (ScriptException | NoSuchMethodException e) {
fail(e, "js引擎执行失败");
}
}).onFailure(handleFail(SHARE_URL_PREFIX));
}
}).onFailure(handleFail(SHARE_URL_PREFIX));
}).onFailure(handleFail(sUrl));
return promise.future();
}
private String getJsByPwd(String pwd, String html, String subText) {
String jsText = getJsText(html);
if (jsText == null) {
throw new RuntimeException("js脚本匹配失败, 可能分享已失效");
}
jsText = jsText.replace("document.getElementById('pwd').value", "\"" + pwd + "\"");
int i = jsText.indexOf(subText);
if (i > 0) {
jsText = jsText.substring(0, i);
}
return jsText;
}
private String getJsText(String html) {
String jsTagStart = "<script type=\"text/javascript\">";
String jsTagEnd = "</script>";
@@ -102,60 +91,14 @@ public class LzTool extends PanBase {
}
int startPos = index + jsTagStart.length();
int endPos = html.indexOf(jsTagEnd, startPos);
return html.substring(startPos, endPos).replaceAll("<!--.*-->", "");
return html.substring(startPos, endPos);
}
private void getDownURL(String key, WebClient client, Map<String, ?> obj) {
if (obj == null) {
fail("需要访问密码");
return;
}
Map<?, ?> signMap = (Map<?, ?>)obj.get("data");
String url0 = obj.get("url").toString();
private void getDownURL(String key, WebClient client, Map<String, ?> signMap) {
MultiMap map = MultiMap.caseInsensitiveMultiMap();
signMap.forEach((k, v) -> {
map.add((String) k, v.toString());
map.set(k, v.toString());
});
MultiMap headers = HeaderUtils.parseHeaders("""
Accept: application/json, text/javascript, */*
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
Cache-Control: no-cache
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Pragma: no-cache
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/134.0.0.0 Safari/537.36 Edg/134.0.0.0
X-Requested-With: XMLHttpRequest
sec-ch-ua: "Chromium";v="134", "Not:A-Brand";v="24", "Microsoft Edge";v="134"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
""");
headers.set("referer", key);
// action=downprocess&signs=%3Fctdf&websignkey=I5gl&sign=BWMGOF1sBTRWXwI9BjZdYVA7BDhfNAIyUG9UawJtUGMIPlAhACkCa1UyUTAAYFxvUj5XY1E7UGFXaFVq&websign=&kd=1&ves=1
String url = SHARE_URL_PREFIX + url0;
client.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
try {
JsonObject urlJson = asJson(res2);
if (urlJson.getInteger("zt") != 1) {
fail(urlJson.getString("inf"));
return;
}
String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url");
headers.remove("Referer");
client.getAbs(downUrl).putHeaders(headers).send()
.onSuccess(res3 -> promise.complete(res3.headers().get("Location")))
.onFailure(handleFail(downUrl));
} catch (Exception e) {
fail("解析异常");
}
}).onFailure(handleFail(url));
}
private static MultiMap getHeaders(String key) {
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
var userAgent2 = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, " +
"like " +
@@ -165,75 +108,18 @@ public class LzTool extends PanBase {
headers.set("sec-ch-ua-platform", "Android");
headers.set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
headers.set("sec-ch-ua-mobile", "sec-ch-ua-mobile");
return headers;
}
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String sUrl = shareLinkInfo.getShareUrl();
String pwd = shareLinkInfo.getSharePassword();
WebClient client = clientNoRedirects;
client.getAbs(sUrl).send().onSuccess(res -> {
String html = res.bodyAsString();
try {
String jsText = getJsByPwd(pwd, html, "var urls =window.location.href");
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file");
Map<String, Object> data = CastUtil.cast(scriptObjectMirror.get("data"));
MultiMap map = MultiMap.caseInsensitiveMultiMap();
data.forEach((k, v) -> map.set(k, v.toString()));
log.debug("解析参数: {}", map);
MultiMap headers = getHeaders(sUrl);
String url = SHARE_URL_PREFIX + "/filemoreajax.php?file=" + data.get("fid");
client.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
JsonObject fileListJson = asJson(res2);
if (fileListJson.getInteger("zt") != 1) {
promise.fail(baseMsg() + fileListJson.getString("info"));
return;
}
List<FileInfo> list = new ArrayList<>();
fileListJson.getJsonArray("text").forEach(item -> {
/*
{
"icon": "apk",
"t": 0,
"id": "iULV2n4361c",
"name_all": "xx.apk",
"size": "49.8 M",
"time": "2021-03-19",
"duan": "in4361",
"p_ico": 0
}
*/
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
String size = fileJson.getString("size");
Long sizeNum = FileSizeConverter.convertToBytes(size);
String panType = shareLinkInfo.getType();
String id = fileJson.getString("id");
fileInfo.setFileName(fileJson.getString("name_all"))
.setFileId(id)
.setCreateTime(fileJson.getString("time"))
.setFileType(fileJson.getString("icon"))
.setSizeStr(fileJson.getString("size"))
.setSize(sizeNum)
.setPanType(panType)
.setParserUrl(getDomainName() + "/d/" + panType + "/" + id)
.setPreviewUrl(String.format("%s/v2/view/%s/%s", getDomainName(),
shareLinkInfo.getType(), id));
log.debug("文件信息: {}", fileInfo);
list.add(fileInfo);
});
promise.complete(list);
});
} catch (ScriptException | NoSuchMethodException e) {
promise.fail(e);
String url = SHARE_URL_PREFIX + "/ajaxm.php";
client.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
JsonObject urlJson = res2.bodyAsJsonObject();
if (urlJson.getInteger("zt") != 1) {
fail(urlJson.getString("inf"));
return;
}
});
return promise.future();
String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url");
client.getAbs(downUrl).putHeaders(headers).send()
.onSuccess(res3 -> promise.complete(res3.headers().get("Location")))
.onFailure(handleFail(downUrl));
}).onFailure(handleFail(url));
}
}

View File

@@ -1,125 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.json.JsonObject;
import io.vertx.uritemplate.UriTemplate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 酷狗音乐分享
* <a href="https://t1.kugou.com/song.html?id=2bi8Fe9CSV3">分享链接1</a>
* <a href="https://www.kugou.com/share/2bi8Fe9CSV3.html?id=2bi8Fe9CSV3#6ed9gna4">分享链接2</a>
* <a href="https://www.kugou.com/share/2bi8Fe9CSV3.html">分享链接3</a>
* <a href="https://www.kugou.com/mixsong/8odv4ef8.html">歌曲链接1</a>
*/
public class MkgsTool extends PanBase {
public static final String API_URL = "https://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash={hash}";
private static final MultiMap headers = MultiMap.caseInsensitiveMultiMap();
static {
// 设置 User-Agent
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0");
// 设置 Accept
headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
// 设置 If-Modified-Since
headers.set("If-Modified-Since", "Mon, 21 Oct 2024 08:45:50 GMT");
// 设置 Priority
headers.set("Priority", "u=0, i");
// 设置 Sec-CH-UA
headers.set("Sec-CH-UA", "\"Microsoft Edge\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"");
// 设置 Sec-CH-UA-Mobile
headers.set("Sec-CH-UA-Mobile", "?0");
// 设置 Sec-CH-UA-Platform
headers.set("Sec-CH-UA-Platform", "\"Windows\"");
// 设置 Sec-Fetch-Dest
headers.set("Sec-Fetch-Dest", "document");
// 设置 Sec-Fetch-Mode
headers.set("Sec-Fetch-Mode", "navigate");
// 设置 Sec-Fetch-Site
headers.set("Sec-Fetch-Site", "none");
// 设置 Sec-Fetch-User
headers.set("Sec-Fetch-User", "?1");
// 设置 Upgrade-Insecure-Requests
headers.set("Upgrade-Insecure-Requests", "1");
};
public MkgsTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
String shareUrl = shareLinkInfo.getStandardUrl();
// String shareUrl = "https://t1.kugou.com/song.html?id=2bi8Fe9CSV3";
clientNoRedirects.getAbs(shareUrl).send().onSuccess(res -> {
String locationURL = res.headers().get("Location");
downUrl(locationURL);
}).onFailure(handleFail(shareUrl));
return promise.future();
}
protected void downUrl(String locationURL) {
client.getAbs(locationURL).putHeaders(headers).send().onSuccess(res2->{
String body = res2.bodyAsString();
// 正则表达式匹配 hash 字段
String regex = "\"hash\"\s*:\s*\"([A-F0-9]+)\"";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(body);
// 查找并输出 hash 字段的值
if (matcher.find()) {
String hashValue = matcher.group(1); // 获取第一个捕获组
System.out.println(hashValue);
client.getAbs(UriTemplate.of(API_URL)).setTemplateParam("hash", hashValue).send().onSuccess(res3 -> {
JsonObject jsonObject = asJson(res3);
System.out.println(jsonObject.encodePrettily());
if (jsonObject.containsKey("url")) {
promise.complete(jsonObject.getString("url"));
} else {
fail("下载链接不存在");
}
}).onFailure(handleFail(API_URL.replace("{hash}", hashValue)));
} else {
fail("歌曲hash匹配失败, 可能分享已失效");
}
}).onFailure(handleFail(locationURL));
}
public static class MkgTool extends MkgsTool {
public MkgTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
@Override
public Future<String> parse() {
downUrl(shareLinkInfo.getStandardUrl());
return promise.future();
}
;
}
public static class Mkgs2Tool extends MkgTool {
public Mkgs2Tool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
}
}

View File

@@ -1,68 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.JsExecUtils;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import io.vertx.uritemplate.UriTemplate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 酷我音乐分享
* <a href="https://kuwo.cn/play_detail/395500809">分享示例</a>
* <a href="https://m.kuwo.cn/newh5app/play_detail/318448522">分享示例</a>
*/
public class MkwTool extends PanBase {
public static final String API_URL = "https://www.kuwo.cn/api/v1/www/music/playUrl?mid={mid}&type=music&httpsStatus=1&reqId=&plat=web_www&from=";
public MkwTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
String shareUrl = shareLinkInfo.getStandardUrl();
clientSession.getAbs(shareUrl).send().onSuccess(result -> {
String cookie = result.headers().get("set-cookie");
if (!cookie.isEmpty()) {
String regex = "([A-Za-z0-9_]+)=([A-Za-z0-9]+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(cookie);
if (matcher.find()) {
System.out.println(matcher.group(1));
System.out.println(matcher.group(2));
var key = matcher.group(1);
var token = matcher.group(2);
String sign = JsExecUtils.getKwSign(token, key);
System.out.println(sign);
clientSession.getAbs(UriTemplate.of(API_URL)).setTemplateParam("mid", shareLinkInfo.getShareKey())
.putHeader("Secret", sign).send().onSuccess(res -> {
JsonObject json = asJson(res);
log.debug(json.encodePrettily());
try {
if (json.getInteger("code") == 200) {
complete(json.getJsonObject("data").getString("url"));
} else {
fail("链接已失效/需要VIP");
}
} catch (Exception e) {
e.printStackTrace();
fail("解析失败");
}
});
}
}
});
return promise.future();
}
}

View File

@@ -1,26 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
/**
* 咪咕音乐分享
*/
public class MmgTool extends PanBase {
public static final String API_URL = "";
public MmgTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
String shareUrl = shareLinkInfo.getStandardUrl();
// TODO
promise.complete("暂未实现, 敬请期待");
return promise.future();
}
}

View File

@@ -1,59 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.URLUtil;
import io.vertx.core.Future;
import io.vertx.uritemplate.UriTemplate;
/**
* 网易云音乐, 单歌曲直链解析
* <a href="http://163cn.tv/ykLZJJT">示例分享1</a>
* <a href="https://music.163.com/#/song?id=472194327">示例分享2</a>
*/
public class MnesTool extends PanBase {
public static final String API_URL = "https://music.163.com/song/media/outer/url?id={id}";
public MnesTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
String shareUrl = shareLinkInfo.getStandardUrl();
clientNoRedirects.getAbs(shareUrl).send().onSuccess(res -> {
String locationURL = res.headers().get("Location");
downUrl(locationURL);
}).onFailure(handleFail(shareUrl));
return promise.future();
}
protected void downUrl(String locationURL) {
String id = URLUtil.from(locationURL).getParam("id");
clientNoRedirects.getAbs(UriTemplate.of(API_URL)).setTemplateParam("id", id).send()
.onSuccess(res2 -> {
String location = res2.headers().get("Location");
if (location.endsWith("/404")) {
fail("链接已失效: id={}", id);
} else {
promise.complete(location);
}
}).onFailure(handleFail(API_URL.replace("{id}", id)));
}
public static class MneTool extends MnesTool{
public MneTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
@Override
public Future<String> parse() {
downUrl(shareLinkInfo.getStandardUrl());
return promise.future();
}
}
}

View File

@@ -1,78 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.URLUtil;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import io.vertx.uritemplate.UriTemplate;
/**
*
* QQ音乐分享解析
* <a href="https://c6.y.qq.com/base/fcgi-bin/u?__=w3lqEpOHACLO">分享示例</a>
* <a href="https://y.qq.com/n/ryqq/songDetail/000XjcLg0fbRjv?songtype=0">详情页</a>
*/
public class MqqsTool extends PanBase {
public static final String API_URL = "https://u.y.qq.com/cgi-bin/musicu" +
".fcg?-=getplaysongvkey2682247447678878&g_tk=5381&loginUin=956581739&hostUin=0&format=json&inCharset=utf8" +
"&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data=%7B%22req_0%22%3A%7B%22module%22%3A" +
"%22vkey.GetVkeyServer%22%2C%22method%22%3A%22CgiGetVkey%22%2C%22param%22%3A%7B%22guid%22%3A%222796982635" +
"%22%2C%22songmid%22%3A%5B%22{songmid}%22%5D%2C%22songtype%22%3A%5B1%5D%2C%22uin%22%3A%22956581739%22" +
"%2C%22loginflag%22%3A1%2C%22platform%22%3A%2220%22%7D%7D%2C%22comm%22%3A%7B%22uin%22%3A956581739%2C" +
"%22format%22%3A%22json%22%2C%22ct%22%3A24%2C%22cv%22%3A0%7D%7D";
public MqqsTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
String shareUrl = shareLinkInfo.getStandardUrl();
// https://c6.y.qq.com/base/fcgi-bin/u?__=uXXtsB
// String shareUrl = "https://c6.y.qq.com/base/fcgi-bin/u?__=k8gafY6HAQ5Y";
clientNoRedirects.getAbs(shareUrl).send().onSuccess(res -> {
String locationURL = res.headers().get("Location");
String id = URLUtil.from(locationURL).getParam("songmid");
downUrl(id);
}).onFailure(handleFail(shareUrl));
return promise.future();
}
protected void downUrl(String id) {
clientNoRedirects.getAbs(UriTemplate.of(API_URL)).setTemplateParam("songmid", id).send().onSuccess(res2 -> {
JsonObject jsonObject = asJson(res2);
log.debug(jsonObject.encodePrettily());
try {
JsonObject data = jsonObject.getJsonObject("req_0").getJsonObject("data");
String path = data.getJsonArray("midurlinfo").getJsonObject(0).getString("purl");
if (path.isEmpty()) {
fail("暂不支持VIP音乐");
return;
}
String downURL = data.getJsonArray("sip").getString(0)
.replace("http://", "https://") + path;
promise.complete(downURL);
} catch (Exception e) {
fail("获取失败");
}
}).onFailure(handleFail(API_URL.replace("{id}", id)));
}
public static class MqqTool extends MqqsTool{
public MqqTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
@Override
public Future<String> parse() {
downUrl(shareLinkInfo.getShareKey());
return promise.future();
}
}
}

View File

@@ -1,22 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
/**
* 其他网盘解析
*/
public class OtherTool extends PanBase {
private static final String API_URL_PREFIX = "";
public OtherTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
// TODO
fail("暂未实现, 敬请期待");
return future();
}
}

View File

@@ -1,90 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.json.JsonObject;
import io.vertx.uritemplate.UriTemplate;
/**
* 115网盘
*
* 需要请求API的UA和请求下载链接的UA保持一致安卓Chrome需要访问电脑版才能下载
*/
public class P115Tool extends PanBase {
private static final String API_URL_PREFIX = "https://115cdn.com/webapi/";
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "share/snap?share_code={dataKey}&offset=0" +
"&limit=20&receive_code={dataPwd}&cid=";
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "share/skip_login_downurl";
private static final MultiMap header;
static {
header = MultiMap.caseInsensitiveMultiMap();
header.set("Accept", "application/json, text/plain, */*");
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
header.set("Content-Length", "0");
header.set("DNT", "1");
header.set("Host", "115cdn.com");
header.set("Origin", "https://115cdn.com");
header.set("Pragma", "no-cache");
header.set("Referer", "https://115cdn.com");
header.set("Sec-Fetch-Dest", "empty");
header.set("Sec-Fetch-Mode", "cors");
header.set("Sec-Fetch-Site", "cross-site");
header.set("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
header.set("sec-ch-ua-mobile", "?0");
header.set("sec-ch-ua-platform", "\"Windows\"");
}
public P115Tool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
// 第一次请求 获取文件信息
client.getAbs(UriTemplate.of(FIRST_REQUEST_URL))
.putHeaders(header)
.putHeader("User-Agent", shareLinkInfo.getOtherParam().get("UA").toString())
.setTemplateParam("dataKey", shareLinkInfo.getShareKey())
.setTemplateParam("dataPwd", shareLinkInfo.getSharePassword())
.send().onSuccess(res -> {
JsonObject resJson = asJson(res);
if (!resJson.getBoolean("state")) {
fail(FIRST_REQUEST_URL + " 解析错误: " + resJson);
return;
}
// 文件Id: data.list[0].fid
JsonObject fileInfo = resJson.getJsonObject("data").getJsonArray("list").getJsonObject(0);
String fileId = fileInfo.getString("fid");
// 第二次请求
// share_code={dataKey}&receive_code={dataPwd}&file_id={file_id}
client.postAbs(SECOND_REQUEST_URL)
.putHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.putHeader("User-Agent", shareLinkInfo.getOtherParam().get("UA").toString())
.sendForm(MultiMap.caseInsensitiveMultiMap()
.set("share_code", shareLinkInfo.getShareKey())
.set("receive_code", shareLinkInfo.getSharePassword())
.set("file_id", fileId))
.onSuccess(res2 -> {
JsonObject resJson2 = asJson(res2);
if (!resJson2.getBoolean("state")) {
fail(FIRST_REQUEST_URL + " 解析错误: " + resJson2);
return;
}
// data.url.url
promise.complete(resJson2.getJsonObject("data").getJsonObject("url").getString("url"));
}).onFailure(handleFail(SECOND_REQUEST_URL));
}).onFailure(handleFail(FIRST_REQUEST_URL));
return promise.future();
}
}

View File

@@ -1,50 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 118网盘解析
*/
public class P118Tool extends PanBase {
private static final String API_URL_PREFIX = "https://qaiu.118pan.com/ajax.php";
// private static final String
public P118Tool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
client.postAbs(API_URL_PREFIX)
.putHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.sendBuffer(Buffer.buffer("action=load_down_addr1&file_id=" + shareLinkInfo.getShareKey()))
.onSuccess(res -> {
System.out.println(res.headers());
Pattern compile = Pattern.compile("href=\"([^\"]+)\"");
Matcher matcher = compile.matcher(res.bodyAsString());
if (matcher.find()) {
//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();
}
}

View File

@@ -1,109 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <a href="https://yunpan.360.cn">360AI云盘</a>
* 360AI云盘解析
* 下载链接需要Referer: https://link.yunpan.com/
*/
public class P360Tool extends PanBase {
public P360Tool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
// https://www.yunpan.com/surl_yD7jK9d4W6D 获取302跳转地址
clientNoRedirects.getAbs(shareLinkInfo.getShareUrl()).send()
.onSuccess(res -> {
String location = res.getHeader("Location");
if (location != null) {
down302(location);
} else {
fail();
}
}).onFailure(handleFail(""));
return future();
}
private void down302(String url) {
// 获取URL前缀 https://214004.link.yunpan.com/lk/surl_yD7ZU4WreR8 -> https://214004.link.yunpan.com/
String urlPrefix = url.substring(0, url.indexOf("/", 8));
clientSession.getAbs(url)
.send()
.onSuccess(res -> {
// find "nid": "17402043311959599"
Pattern compile = Pattern.compile("\"nid\": \"([^\"]+)\"");
Matcher matcher = compile.matcher(res.bodyAsString());
AtomicReference<String> nid = new AtomicReference<>();
if (matcher.find()) {
nid.set(matcher.group(1));
} else {
// 需要验证密码
/*
* POST https://4aec17.link.yunpan.com/share/verifyPassword
* Content-type: application/x-www-form-urlencoded UTF-8
* Referer: https://4aec17.link.yunpan.com/lk/surl_yD7jK9d4W6D
*
* shorturl=surl_yD7jK9d4W6D&linkpassword=d969
*/
clientSession.postAbs(urlPrefix + "/share/verifyPassword")
.putHeader("Content-Type", "application/x-www-form-urlencoded")
.putHeader("Referer", urlPrefix)
.sendBuffer(Buffer.buffer("shorturl=" + shareLinkInfo.getShareKey() + "&linkpassword" +
"=" + shareLinkInfo.getSharePassword()))
.onSuccess(res2 -> {
JsonObject entries = asJson(res2);
if (entries.getInteger("errno") == 0) {
clientSession.getAbs(url)
.send()
.onSuccess(res3 -> {
Matcher matcher1 = compile.matcher(res3.bodyAsString());
if (matcher1.find()) {
nid.set(matcher1.group(1));
} else {
fail();
return;
}
down(urlPrefix, nid.get());
}).onFailure(handleFail(""));
} else {
fail(entries.encode());
}
}).onFailure(handleFail(""));
return;
}
down(urlPrefix, nid.get());
}).onFailure(handleFail(""));
}
private void down(String urlPrefix, String nid) {
clientSession.postAbs(urlPrefix + "/share/downloadfile")
.putHeader("Content-Type", "application/x-www-form-urlencoded")
.putHeader("Referer", urlPrefix)
.sendBuffer(Buffer.buffer("shorturl=" + shareLinkInfo.getShareKey() + "&nid=" + nid))
.onSuccess(res2 -> {
JsonObject entries = asJson(res2);
String downloadurl = entries.getJsonObject("data").getString("downloadurl");
complete(downloadurl);
}).onFailure(handleFail(""));
}
// public static void main(String[] args) {
// String s = new P360Tool(ShareLinkInfo.newBuilder().shareUrl("https://www.yunpan.com/surl_yD7ZU4WreR8")
// .shareKey("surl_yD7ZU4WreR8")
// .build()).parseSync();
// System.out.println(s);
// }
}

View File

@@ -1,47 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.json.JsonObject;
import io.vertx.uritemplate.UriTemplate;
/**
* <a href="https://passport2.chaoxing.com">超星云盘</a>
*/
public class PcxTool extends PanBase {
public PcxTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
client.getAbs(shareLinkInfo.getShareUrl())
.send().onSuccess(res -> {
// 'download': 'https://d0.ananas.chaoxing.com/download/de08dcf546e4dd88a17bead86ff6338d?at_=1740211698795&ak_=d62a3acbd5ce43e1e8565b67990691e4&ad_=8c4ef22e980ee0dd9532ec3757ab19f8&fn=33.c'
String body = res.bodyAsString();
// 获取download
String str = "var fileinfo = {";
String fileInfo = res.bodyAsString().substring(res.bodyAsString().indexOf(str) + str.length() - 1
, res.bodyAsString().indexOf("};") + 1);
fileInfo = fileInfo.replace("'", "\"");
JsonObject jsonObject = new JsonObject(fileInfo);
String download = jsonObject.getString("download");
if (download.contains("fn=")) {
complete(download);
} else {
fail("获取下载链接失败: 不支持的文件类型: {}", jsonObject.getString("suffix"));
}
}).onFailure(handleFail(shareLinkInfo.getShareUrl()));
return promise.future();
}
// public static void main(String[] args) {
// String s = new PcxTool(ShareLinkInfo.newBuilder().shareUrl("https://pan-yz.cldisk.com/external/m/file/953658049102462976")
// .shareKey("953658049102462976")
// .build()).parseSync();
// System.out.println(s);
// }
}

View File

@@ -1,95 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.URLUtil;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.ProxyOptions;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* <a href="https://www.dropbox.com/">dropbox</a>
* Dropbox网盘--不支持大陆地区
*/
public class PdbTool extends PanBase implements IPanTool {
private static final String API_URL =
"https://www.dropbox.com/sharing/fetch_user_content_link";
static final String COOKIE_KEY = "__Host-js_csrf=";
public PdbTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
@Override
public Future<String> parse() {
// https://www.dropbox.com/scl/fi/cwnbms1yn8u6rcatzyta7/emqx-5.0.26-el7-amd64.tar.gz?rlkey=3uoi4bxz5mv93jmlaws0nlol1&e=8&st=fe0lclc2&dl=0
// https://www.dropbox.com/scl/fi/()/Get-Started-with-Dropbox.pdf?rlkey=yrddd6s9gxsq967pmbgtzvfl3&st=2trcc1f3&dl=0
//
clientSession.getAbs(shareLinkInfo.getShareUrl())
.send()
.onSuccess(res->{
List<String> collect =
res.cookies().stream().filter(key -> key.contains(COOKIE_KEY)).toList();
if (collect.isEmpty()) {
fail("cookie未找到");
return;
}
Matcher matcher = Pattern.compile(COOKIE_KEY + "([\\w-]+);").matcher(collect.get(0));
String _t;
if (matcher.find()) {
_t = matcher.group(1);
} else {
fail("cookie未找到");
return;
}
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
headers.set("accept", "*/*");
headers.set("accept-language", "zh-CN,zh;q=0.9");
headers.set("cache-control", "no-cache");
headers.set("dnt", "1");
headers.set("origin", "https://www.dropbox.com");
headers.set("pragma", "no-cache");
headers.set("priority", "u=1, i");
headers.set("referer", shareLinkInfo.getShareUrl());
headers.set("sec-ch-ua", "\"Chromium\";v=\"130\", \"Microsoft Edge\";v=\"130\", \"Not?A_Brand\";v=\"99\"");
headers.set("sec-ch-ua-mobile", "?0");
headers.set("sec-ch-ua-platform", "\"Windows\"");
headers.set("sec-fetch-dest", "empty");
headers.set("sec-fetch-mode", "cors");
headers.set("sec-fetch-site", "same-origin");
headers.set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0");
headers.set("x-dropbox-client-yaps-attribution", "edison_atlasservlet.file_viewer-edison:prod");
headers.set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
try {
URL url = new URL(shareLinkInfo.getShareUrl());
// https://www.dropbox.com/scl/fi/cwnbms1yn8u6rcatzyta7/xxx?rlkey=xx&dl=0
String u0 = URLEncoder.encode((url.getProtocol() + "://" + url.getHost() + url.getPath() + "?rlkey=%s&dl=0")
.formatted(URLUtil.from(shareLinkInfo.getShareUrl()).getParam("rlkey")), StandardCharsets.UTF_8);
clientSession.postAbs(API_URL)
.sendBuffer(Buffer.buffer("is_xhr=true&t=%s&url=%s&origin=PREVIEW_PAGE".formatted(_t, u0)))
.onSuccess(res2 -> {
complete(res2.bodyAsString());
})
.onFailure(handleFail());
} catch (Exception e) {
e.printStackTrace();
}
})
.onFailure(handleFail("请求下载链接失败"));
return future();
}
}

View File

@@ -1,98 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.ProxyOptions;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <a href="https://drive.google.com/">GoogleDrive</a>
* Google Drive文件解析工具.
*/
public class PgdTool extends PanBase implements IPanTool {
private static final String DOWN_URL_TEMPLATE =
"https://drive.usercontent.google.com/download";
private String downloadUrl;
public PgdTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
@Override
public Future<String> parse() {
downloadUrl = DOWN_URL_TEMPLATE + "?id=" + shareLinkInfo.getShareKey() + "&export=download";
if (shareLinkInfo.getOtherParam().containsKey("proxy")) {
// if (shareLinkInfo.getOtherParam().containsKey("bypassCheck")
// && "true".equalsIgnoreCase(shareLinkInfo.getOtherParam().get("bypassCheck").toString())) {
// 发起请求但不真正下载文件, 只检查响应头
client.headAbs(downloadUrl).send()
.onSuccess(this::handleResponse)
.onFailure(handleFail("请求下载链接失败"));
return future();
}
complete(downloadUrl);
return future();
}
/**
* 处理下载链接的响应.
*/
private void handleResponse(HttpResponse<Buffer> response) {
String contentType = response.getHeader("Content-Type");
if (contentType != null && !contentType.contains("text/html")) {
complete(downloadUrl);
} else {
// 如果不是文件流类型,从 HTML 中解析出真实下载链接
client.getAbs(downloadUrl)
.send()
.onSuccess(res0 -> {
parseHtmlForRealLink(res0.bodyAsString());
})
.onFailure(handleFail("请求下载链接失败"));
}
}
/**
* 从HTML内容中解析真实下载链接.
*/
private void parseHtmlForRealLink(String html) {
// 使用正则表达式匹配 id、export、confirm、uuid、at 等参数
String id = extractHiddenInputValue(html, "id");
String confirm = extractHiddenInputValue(html, "confirm");
String uuid = extractHiddenInputValue(html, "uuid");
String at = extractHiddenInputValue(html, "at");
if (id != null && confirm != null && uuid != null) {
String realDownloadLink = DOWN_URL_TEMPLATE +
"?id=" + id +
"&export=download" +
"&confirm=" + confirm +
"&uuid=" + uuid;
if (at != null) {
realDownloadLink += ( "&at=" + at);
}
complete(realDownloadLink);
} else {
fail("无法找到完整的下载链接参数");
}
}
/**
* 辅助方法: 从HTML中提取指定name的input隐藏字段的value
*/
private String extractHiddenInputValue(String html, String name) {
Pattern pattern = Pattern.compile("<input[^>]*name=\"" + name + "\"[^>]*value=\"([^\"]*)\"");
Matcher matcher = pattern.matcher(html);
return matcher.find() ? matcher.group(1) : null;
}
}

View File

@@ -1,58 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
/**
* <a href="https://www.icloud.com.cn/">iCloud云盘(pic)</a>
*/
public class PicTool extends PanBase {
private static final String api = "https://ckdatabasews.icloud.com.cn/database/1/com.apple.cloudkit/production/public/records/resolve";
public PicTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
// {"shortGUIDs":[{"value":"xxx"}]}
JsonObject jsonObject =
new JsonObject("{\"shortGUIDs\":[{\"value\":\"%s\"}]}".formatted(shareLinkInfo.getShareKey()));
client.postAbs(api).sendJsonObject(jsonObject).onSuccess(res -> {
// results->rootRecord->fields->fileContent->value->downloadURL // ${f}->fileName
// fileName: results->share->fields->cloudkit.title->value + "." + results->rootRecord->fields->extension->value
JsonObject json = asJson(res);
try {
JsonObject result = json.getJsonArray("results").getJsonObject(0);
JsonObject fileInfo = result
.getJsonObject("rootRecord")
.getJsonObject("fields");
String downURL = fileInfo
.getJsonObject("fileContent")
.getJsonObject("value")
.getString("downloadURL");
String extension = fileInfo
.getJsonObject("extension")
.getString("value");
String fileTitle = result
.getJsonObject("share")
.getJsonObject("fields")
.getJsonObject("cloudkit.title")
.getString("value");
complete(downURL.replace("${f}", fileTitle + "." + extension));
} catch (Exception e) {
fail(e, "json解析失败");
}
}).onFailure(handleFail());
return promise.future();
}
}

View File

@@ -1,231 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.WorkerExecutor;
import io.vertx.core.json.JsonObject;
import io.vertx.uritemplate.UriTemplate;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <a href="https://onedrive.live.com/">onedrive分享(od)</a>
*/
public class PodTool extends PanBase {
/*
* https://1drv.ms/w/s!Alg0feQmCv2rnRFd60DQOmMa-Oh_?e=buaRtp --302->
* https://api.onedrive.com/v1.0/drives/abfd0a26e47d3458/items/ABFD0A26E47D3458!3729?authkey=!AF3rQNA6Yxr46H8
* https://onedrive.live.com/redir?resid=(?<cid>)!(?<cid2>)&authkey=(?<authkey>)&e=hV98W1
* cid: abfd0a26e47d3458, cid2: ABFD0A26E47D3458!3729 authkey: !AF3rQNA6Yxr46H8
* -> @content.downloadUrl
*/
// https://onedrive.live.com/redir?resid=ABFD0A26E47D3458!4699&e=OggA4s&migratedtospo=true&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3UvcyFBbGcwZmVRbUN2MnJwRnZ1NDQ0aGc1eVZxRGNLP2U9T2dnQTRz
private static final String API_TEMPLATE = "https://onedrive.live.com/embed" +
"?id={resid}&resid={resid1}" +
"&cid={cid}" +
"&redeem={redeem}" +
"&migratedtospo=true&embed=1";
private static final String TOKEN_API = "https://api-badgerp.svc.ms/v1.0/token";
private static final Pattern redirectUrlRegex =
Pattern.compile("resid=(?<cid1>[^!]+)!(?<cid2>[^&]+).+&redeem=(?<redeem>.+).*");
public PodTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
/*
* POST https://api-badgerp.svc.ms/v1.0/token
* Content-Type: application/json
*
* {
* "appid": "00000000-0000-0000-0000-0000481710a4"
* }
*/
// https://my.microsoftpersonalcontent.com/_api/v2.0/shares/u!aHR0cHM6Ly8xZHJ2Lm1zL3UvcyFBbGcwZmVRbUN2MnJwRnZ1NDQ0aGc1eVZxRGNLP2U9T2dnQTRz/driveitem?%24select=*%2Cocr%2CwebDavUrl
// https://onedrive.live.com/embed?id=ABFD0A26E47D3458!4698&resid=ABFD0A26E47D3458!4698&cid=abfd0a26e47d3458&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3UvYy9hYmZkMGEyNmU0N2QzNDU4L0lRUllOSDNrSmdyOUlJQ3JXaElBQUFBQUFTWGlubWZ2WmNxYUQyMXJUQjIxVmg4&migratedtospo=true&embed=1
clientNoRedirects.getAbs(shareLinkInfo.getShareUrl() == null ? shareLinkInfo.getStandardUrl() :
shareLinkInfo.getShareUrl()).send().onSuccess(r0 -> {
String location = r0.getHeader("Location");
Matcher matcher = redirectUrlRegex.matcher(location);
if (!matcher.find()) {
fail("Location格式错误");
return;
}
String redeem = matcher.group("redeem");
String cid1 = matcher.group("cid1");
String cid2 = cid1 + "!" + matcher.group("cid2");
clientNoRedirects.getAbs(UriTemplate.of(API_TEMPLATE))
.setTemplateParam("resid", cid2)
.setTemplateParam("resid1", cid2)
.setTemplateParam("cid", cid1.toLowerCase())
.setTemplateParam("redeem", redeem)
.send()
.onSuccess(r1 -> {
String auth =
r1.cookies().stream().filter(c -> c.startsWith("BadgerAuth=")).findFirst().orElse("");
if (auth.isEmpty()) {
fail("Error BadgerAuth not fount");
return;
}
String token = auth.split(";")[0].split("=")[1];
try {
String url = matcherUrl(r1.bodyAsString());
sendHttpRequest(url, token).onSuccess(body -> {
Matcher matcher1 =
Pattern.compile("\"downloadUrl\":\"(?<url>https?://[^\s\"]+)").matcher(body);
if (matcher1.find()) {
complete(matcher1.group("url"));
} else {
fail();
}
}).onFailure(handleFail());
} catch (Exception ignored) {
sendHttpRequest2(token, redeem).onSuccess(res -> {
try {
complete(new JsonObject(res).getString("@content.downloadUrl"));
} catch (Exception ignored1) {
fail();
}
}).onFailure(handleFail());
}
}).onFailure(handleFail());
}).onFailure(handleFail());
return promise.future();
}
private String matcherUrl(String html) {
// 正则表达式来匹配 URL
String urlRegex = "'action'.+(?<url>https://.+)'\\)";
Pattern urlPattern = Pattern.compile(urlRegex);
Matcher urlMatcher = urlPattern.matcher(html);
if (urlMatcher.find()) {
String url = urlMatcher.group("url");
System.out.println("URL: " + url);
return url;
}
throw new RuntimeException("URL匹配失败");
}
private String matcherToken(String html) {
// 正则表达式来匹配 inputElem.value 中的 Token
String tokenRegex = "inputElem\\.value\\s*=\\s*'([^']+)'";
Pattern tokenPattern = Pattern.compile(tokenRegex);
Matcher tokenMatcher = tokenPattern.matcher(html);
if (tokenMatcher.find()) {
String token = tokenMatcher.group(1);
System.out.println("Token: " + token);
return token;
}
throw new RuntimeException("token匹配失败");
}
public Future<String> sendHttpRequest2(String token, String redeem) {
Promise<String> promise = Promise.promise();
// 构造 HttpClient
HttpClient client = HttpClient.newHttpClient();
// 构造请求的 URI 和头部信息
// https://onedrive.live.com/redir?cid=abfd0a26e47d3458&resid=ABFD0A26E47D3458!4465&ithint=file%2cxlsx&e=Ao2uSU&migratedtospo=true&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3gvYy9hYmZkMGEyNmU0N2QzNDU4L0VWZzBmZVFtQ3YwZ2dLdHhFUUFBQUFBQlRQRWVDMTZfZk1EYk5FTjhEdTRta1E_ZT1BbzJ1U1U
String url = ("https://my.microsoftpersonalcontent.com/_api/v2.0/shares/u!%s/driveItem?$select=content" +
".downloadUrl").formatted(redeem);
String authorizationHeader = "Badger " + token;
// 构建请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", authorizationHeader)
.build();
// 发送请求并处理响应
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> {
System.out.println("Response Status Code: " + response.statusCode());
System.out.println("Response Body: " + response.body());
promise.complete(response.body());
return null;
});
return promise.future();
}
public Future<String> sendHttpRequest(String url, String token) {
// 创建一个 WorkerExecutor 用于异步执行阻塞的 HTTP 请求
WorkerExecutor executor = WebClientVertxInit.get().createSharedWorkerExecutor("http-client-worker");
Promise<String> promise = Promise.promise();
executor.executeBlocking(() -> {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = null;
try {
// 构造请求
request = HttpRequest.newBuilder()
.uri(new URI(url))
.header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9," +
"image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;" +
"v=b3;q=0.7")
.header("accept-language", "zh-CN,zh;q=0.9")
.header("cache-control", "no-cache")
.header("content-type", "application/x-www-form-urlencoded")
.header("dnt", "1")
.header("origin", "https://onedrive.live.com")
.header("pragma", "no-cache")
.header("priority", "u=0, i")
.header("referer", "https://onedrive.live.com/")
.header("sec-ch-ua", "\"Chromium\";v=\"130\", \"Google Chrome\";v=\"130\", " +
"\"Not?A_Brand\";v=\"99\"")
.header("sec-ch-ua-mobile", "?0")
.header("sec-ch-ua-platform", "\"Windows\"")
.header("sec-fetch-dest", "iframe")
.header("sec-fetch-mode", "navigate")
.header("sec-fetch-site", "cross-site")
.header("upgrade-insecure-requests", "1")
.header("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537" +
".36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36")
.POST(HttpRequest.BodyPublishers.ofString("badger_token=" + token))
.build();
// 发起请求并获取响应
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// 返回响应体
promise.complete(response.body());
return null;
} catch (URISyntaxException | IOException | InterruptedException e) {
throw new RuntimeException(e);
}
});
return promise.future();
}
}

View File

@@ -1,221 +0,0 @@
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;
/**
* 微雨云
*/
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
cache-control: no-cache
dnt: 1
origin: https://www.vyuyun.com
pragma: no-cache
priority: u=1, i
referer: https://www.vyuyun.com/
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-site
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 {
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(apiUrl))
.setTemplateParam("key", shareLinkInfo.getShareKey())
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
.setTemplateParam("id", id)
.putHeaders(header).send().onSuccess(res2 -> {
try {
// data->downInfo->url
String url =
asJson(res2).getJsonObject("data").getJsonObject("downInfo").getString("url");
complete(url);
} catch (Exception ignored) {
fail(asJson(res2).encodePrettily());
}
});
} catch (Exception ignored) {
fail();
}
});
}
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

@@ -1,47 +1,43 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.StringUtils;
import io.netty.handler.codec.http.QueryStringDecoder;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import io.vertx.ext.web.client.WebClient;
/**
* <a href="https://wx.mail.qq.com/">QQ邮箱</a>
*/
public class QQTool extends PanBase {
public class QQTool extends PanBase implements IPanTool {
public static final String REDIRECT_URL_TEMP = "https://iwx.mail.qq.com/ftn/download?func=4&key={key}&code={code}";
public static final String SHARE_URL_PREFIX = "wx.mail.qq.com/ftn/download?";
public QQTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public QQTool(String key, String pwd) {
super(key, pwd);
}
@SuppressWarnings("unchecked")
public Future<String> parse() {
// QQ mail 直接替换为302链接 无需请求
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(shareLinkInfo.getShareUrl(), StandardCharsets.UTF_8);
Map<String, List<String>> prms = queryStringDecoder.parameters();
if (prms.containsKey("key") && prms.containsKey("code") && prms.containsKey("func")) {
log.info(prms.get("func").get(0));
promise.complete(REDIRECT_URL_TEMP.replace("{key}",
prms.get("key").get(0)).replace("{code}", prms.get("code").get(0)));
} else {
fail("key 不合法");
WebClient httpClient = this.client;
// 补全链接
if (!this.key.startsWith("https://" + SHARE_URL_PREFIX)) {
if (this.key.startsWith(SHARE_URL_PREFIX)) {
this.key = "https://" + this.key;
} else if (this.key.startsWith("func=")) {
this.key = "https://" + SHARE_URL_PREFIX + this.key;
} else {
throw new UnsupportedOperationException("未知分享类型");
}
}
// 通过请求URL获取文件信息和直链 暂时不需要
// getFileInfo(key);
return promise.future();
}
private void getFileInfo(String key) {
// 设置基础HTTP头部
var userAgent2 = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, " +
"like " +
@@ -54,7 +50,7 @@ public class QQTool extends PanBase {
headers.set("sec-ch-ua-mobile", "sec-ch-ua-mobile");
// 获取下载中转站页面
client.getAbs(key).putHeaders(headers).send().onSuccess(res -> {
httpClient.getAbs(this.key).putHeaders(headers).send().onSuccess(res -> {
if (res.statusCode() == 200) {
String html = res.bodyAsString();
@@ -65,10 +61,10 @@ public class QQTool extends PanBase {
if (filename != null && filesize != null && fileurl != null) {
// 设置所需HTTP头部
headers.set("Referer", "https://" + StringUtils.StringCutNot(key, "https://", "/") + "/");
headers.set("Referer", "https://" + StringUtils.StringCutNot(this.key, "https://", "/") + "/");
headers.set("Host", StringUtils.StringCutNot(fileurl, "https://", "/"));
res.headers().forEach((k, v) -> {
if (k.equalsIgnoreCase("set-cookie")) {
if (k.toLowerCase().equals("set-cookie")) {
headers.set("Cookie", "mail5k=" + StringUtils.StringCutNot(v, "mail5k=", ";") + ";");
}
});
@@ -86,7 +82,9 @@ public class QQTool extends PanBase {
} else {
this.fail("HTTP状态不正确可能是分享链接的方式已更新");
}
}).onFailure(this.handleFail(key));
}).onFailure(this.handleFail(this.key));
return promise.future();
}
}

View File

@@ -1,171 +0,0 @@
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,69 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import io.vertx.core.Future;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class QQwTool extends QQTool {
public QQwTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
@Override
public Future<String> parse() {
client.getAbs(shareLinkInfo.getShareUrl()).send().onSuccess(res -> {
String html = res.bodyAsString();
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);
/*
clientNoRedirects.getAbs(url302).send().onSuccess(res2 -> {
MultiMap headers = res2.headers();
if (headers.contains("Location")) {
promise.complete(headers.get("Location"));
} else {
fail("找不到重定向URL");
}
}).onFailure(handleFail());
*/
} else {
fail("分享链接解析失败, 可能是链接失效");
}
}).onFailure(handleFail());
return promise.future();
}
private Map<String, String> extractVariables(String jsCode) {
Map<String, String> variables = new HashMap<>();
// 正则表达式匹配 var 变量定义
String regex = "\\s+var\\s+(\\w+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^;\\r\\n]*))";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(jsCode);
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,22 +1,19 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class QkTool extends PanBase {
public class QkTool extends PanBase implements IPanTool {
public QkTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public QkTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
final String key = shareLinkInfo.getShareKey();
final String pwd = shareLinkInfo.getSharePassword();
promise.complete("https://lz.qaiu.top");
IntStream.range(0, 1000).forEach(num -> {
clientNoRedirects.getAbs(key).send()
@@ -36,6 +33,14 @@ public class QkTool extends PanBase {
public static void main(String[] args) {
new QkTool("https://pimapi.lenovomm.com/clouddiskapi/v1/shareRedirect?si=12298704&dk" +
"=19ab590770399d4438ea885446e27186cc668cdfa559f5fcc063a1ecf78008e5&pk" +
"=ef45aa4d25c1dcecb631b3394f51539d59cb35c6a40c3911df8ba431ba2a3244&pc=true&ot=ali&ob=sync-cloud-disk" +
"&ok=649593714557087744.dex&fn=classes" +
".dex&ds=8909208&dc=1&bi=asdddsad&ri=&ts=1701235051759&sn" +
"=13dc33749bd9cc108009fa505b3ecca9f358d70874352858475956ba4240e4c3", "")
.parse().onSuccess((res) -> {
});
}
}

View File

@@ -1,7 +1,8 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import io.vertx.core.Future;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
@@ -10,7 +11,7 @@ import io.vertx.uritemplate.UriTemplate;
/**
* UC网盘解析
*/
public class UcTool extends PanBase {
public class UcTool extends PanBase implements IPanTool {
private static final String API_URL_PREFIX = "https://pc-api.uc.cn/1/clouddrive/";
public static final String SHARE_URL_PREFIX = "https://fast.uc.cn/s/";
@@ -23,14 +24,13 @@ public class UcTool extends PanBase {
private static final String THIRD_REQUEST_URL = API_URL_PREFIX + "file/download?entry=ft&fr=pc&pr=UCBrowser";
public UcTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public UcTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
var dataKey = shareLinkInfo.getShareKey();
var passcode = shareLinkInfo.getSharePassword();
var dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key);
var passcode = (pwd == null) ? "" : pwd;
var jsonObject = JsonObject.of("share_for_transfer", true);
jsonObject.put("pwd_id", dataKey);
jsonObject.put("passcode", passcode);
@@ -79,44 +79,4 @@ public class UcTool extends PanBase {
).onFailure(handleFail(FIRST_REQUEST_URL));
return promise.future();
}
public static void main(String[] args) {
// https://dl-uf-zb.pds.uc.cn/l3PNAKfz/64623447/
// 646b0de6e9f13000c9b14ba182b805312795a82a/
// 646b0de6717e1bfa5bb44dd2a456f103c5177850?
// Expires=1737784900&OSSAccessKeyId=LTAI5tJJpWQEfrcKHnd1LqsZ&
// Signature=oBVV3anhv3tBKanHUcEIsktkB%2BM%3D&x-oss-traffic-limit=503316480
// &response-content-disposition=attachment%3B%20filename%3DC%2523%2520Shell%2520%2528C%2523%2520Offline%2520Compiler%2529_2.5.16.apks
// %3Bfilename%2A%3Dutf-8%27%27C%2523%2520Shell%2520%2528C%2523%2520Offline%2520Compiler%2529_2.5.16.apks
//eyJ4OmF1IjoiLSIsIng6dWQiOiI0LU4tNS0wLTYtTi0zLWZ0LTAtMi1OLU4iLCJ4OnNwIjoiMTAwIiwieDp0b2tlbiI6IjQtZjY0ZmMxMDFjZmQxZGVkNTRkMGM0NmMzYzliMzkyOWYtNS03LTE1MzYxMS1kYWNiMzY2NWJiYWE0ZjVlOWQzNzgwMGVjNjQwMzE2MC0wLTAtMC0wLTQ5YzUzNTE3OGIxOTY0YzhjYzUwYzRlMDk5MTZmYWRhIiwieDp0dGwiOiIxMDgwMCJ9
//eyJjYWxsYmFja0JvZHlUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsImNhbGxiYWNrU3RhZ2UiOiJiZWZvcmUtZXhlY3V0ZSIsImNhbGxiYWNrRmFpbHVyZUFjdGlvbiI6Imlnbm9yZSIsImNhbGxiYWNrVXJsIjoiaHR0cHM6Ly9hdXRoLWNkbi51Yy5jbi9vdXRlci9vc3MvY2hlY2twbGF5IiwiY2FsbGJhY2tCb2R5Ijoie1wiaG9zdFwiOiR7aHR0cEhlYWRlci5ob3N0fSxcInNpemVcIjoke3NpemV9LFwicmFuZ2VcIjoke2h0dHBIZWFkZXIucmFuZ2V9LFwicmVmZXJlclwiOiR7aHR0cEhlYWRlci5yZWZlcmVyfSxcImNvb2tpZVwiOiR7aHR0cEhlYWRlci5jb29raWV9LFwibWV0aG9kXCI6JHtodHRwSGVhZGVyLm1ldGhvZH0sXCJpcFwiOiR7Y2xpZW50SXB9LFwicG9ydFwiOiR7Y2xpZW50UG9ydH0sXCJvYmplY3RcIjoke29iamVjdH0sXCJzcFwiOiR7eDpzcH0sXCJ1ZFwiOiR7eDp1ZH0sXCJ0b2tlblwiOiR7eDp0b2tlbn0sXCJhdVwiOiR7eDphdX0sXCJ0dGxcIjoke3g6dHRsfSxcImR0X3NwXCI6JHt4OmR0X3NwfSxcImhzcFwiOiR7eDpoc3B9LFwiY2xpZW50X3Rva2VuXCI6JHtxdWVyeVN0cmluZy5jbGllbnRfdG9rZW59fSJ9
//callback-var {"x:au":"-","x:ud":"4-N-5-0-6-N-3-ft-0-2-N-N","x:sp":"100","x:token":"4-f64fc101cfd1ded54d0c46c3c9b3929f-5-7-153611-dacb3665bbaa4f5e9d37800ec6403160-0-0-0-0-49c535178b1964c8cc50c4e09916fada","x:ttl":"10800"}
//callback {"callbackBodyType":"application/json","callbackStage":"before-execute","callbackFailureAction":"ignore","callbackUrl":"https://auth-cdn.uc.cn/outer/oss/checkplay","callbackBody":"{\"host\":${httpHeader.host},\"size\":${size},\"range\":${httpHeader.range},\"referer\":${httpHeader.referer},\"cookie\":${httpHeader.cookie},\"method\":${httpHeader.method},\"ip\":${clientIp},\"port\":${clientPort},\"object\":${object},\"sp\":${x:sp},\"ud\":${x:ud},\"token\":${x:token},\"au\":${x:au},\"ttl\":${x:ttl},\"dt_sp\":${x:dt_sp},\"hsp\":${x:hsp},\"client_token\":${queryString.client_token}}"}
/*
// callback-var
{
"x:au": "-",
"x:ud": "4-N-5-0-6-N-3-ft-0-2-N-N",
"x:sp": "100",
"x:token": "4-f64fc101cfd1ded54d0c46c3c9b3929f-5-7-153611-dacb3665bbaa4f5e9d37800ec6403160-0-0-0-0-49c535178b1964c8cc50c4e09916fada",
"x:ttl": "10800"
}
// callback
{
"callbackBodyType": "application/json",
"callbackStage": "before-execute",
"callbackFailureAction": "ignore",
"callbackUrl": "https://auth-cdn.uc.cn/outer/oss/checkplay",
"callbackBody": "{\"host\":${httpHeader.host},\"size\":${size},\"range\":${httpHeader.range},\"referer\":${httpHeader.referer},\"cookie\":${httpHeader.cookie},\"method\":${httpHeader.method},\"ip\":${clientIp},\"port\":${clientPort},\"object\":${object},\"sp\":${x:sp},\"ud\":${x:ud},\"token\":${x:token},\"au\":${x:au},\"ttl\":${x:ttl},\"dt_sp\":${x:dt_sp},\"hsp\":${x:hsp},\"client_token\":${queryString.client_token}}"
}
*/
new UcTool(ShareLinkInfo.newBuilder().shareUrl("https://fast.uc.cn/s/33197dd53ace4").shareKey("33197dd53ace4").build()).parse().onSuccess(
System.out::println
);
}
}

View File

@@ -1,6 +1,11 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
@@ -8,27 +13,33 @@ import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.WebClient;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
/**
* <a href="https://www.wenshushu.cn/">文叔叔</a>
*/
public class WsTool extends PanBase {
public class WsTool extends PanBase implements IPanTool {
public static final String SHARE_URL_PREFIX = "www.wenshushu.cn/f/";
public static final String SHARE_URL_PREFIX2 = "f.ws59.cn/f/";
public static final String SHARE_URL_API = "https://www.wenshushu.cn/ap/";
public WsTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
public WsTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
WebClient httpClient = this.client;
final String key = shareLinkInfo.getShareKey();
final String pwd = shareLinkInfo.getSharePassword();
// 补全链接
if (!this.key.startsWith("https://" + SHARE_URL_PREFIX) && !this.key.startsWith("https://" + SHARE_URL_PREFIX2)) {
if (this.key.startsWith(SHARE_URL_PREFIX) || this.key.startsWith(SHARE_URL_PREFIX2)) {
this.key = "https://" + this.key;
} else if (this.key.matches("^[A-Za-z0-9]+$")) {
this.key = "https://" + SHARE_URL_PREFIX + this.key;
} else {
throw new UnsupportedOperationException("未知分享类型");
}
}
// 设置基础HTTP头部
var userAgent2 = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, " +
@@ -49,23 +60,23 @@ public class WsTool extends PanBase {
if (res.statusCode() == 200) {
try {
// 设置匿名登录token
String token = asJson(res).getJsonObject("data").getString("token");
String token = res.bodyAsJsonObject().getJsonObject("data").getString("token");
headers.set("X-Token", token);
// 获取文件夹信息
httpClient.postAbs(SHARE_URL_API + "task/mgrtask").putHeaders(headers)
.sendJsonObject(JsonObject.of(
"tid", key,
"password", pwd
"tid", StringUtils.StringCutNot(key, this.key.startsWith(SHARE_URL_PREFIX) ? SHARE_URL_PREFIX : SHARE_URL_PREFIX2),
"password", ""
)).onSuccess(res2 -> {
if (res2.statusCode() == 200) {
try {
// 获取文件夹信息
String filetime = asJson(res2).getJsonObject("data").getString("expire"); // 文件夹剩余时间
String filesize = asJson(res2).getJsonObject("data").getString("file_size"); // 文件夹大小
String filepid = asJson(res2).getJsonObject("data").getString("ufileid"); // 文件夹pid
String filebid = asJson(res2).getJsonObject("data").getString("boxid"); // 文件夹bid
String filetime = res2.bodyAsJsonObject().getJsonObject("data").getString("expire"); // 文件夹剩余时间
String filesize = res2.bodyAsJsonObject().getJsonObject("data").getString("file_size"); // 文件夹大小
String filepid = res2.bodyAsJsonObject().getJsonObject("data").getString("ufileid"); // 文件夹pid
String filebid = res2.bodyAsJsonObject().getJsonObject("data").getString("boxid"); // 文件夹bid
// 调试输出文件夹信息
System.out.println("文件夹期限: " + filetime);
@@ -92,9 +103,9 @@ public class WsTool extends PanBase {
if (res3.statusCode() == 200) {
try {
// 获取文件信息
String filename = asJson(res3).getJsonObject("data")
String filename = res3.bodyAsJsonObject().getJsonObject("data")
.getJsonArray("fileList").getJsonObject(0).getString("fname"); // 文件名称
String filefid = asJson(res3).getJsonObject("data")
String filefid = res3.bodyAsJsonObject().getJsonObject("data")
.getJsonArray("fileList").getJsonObject(0).getString("fid"); // 文件fid
// 调试输出文件信息
@@ -112,14 +123,21 @@ public class WsTool extends PanBase {
if (res4.statusCode() == 200) {
try {
// 获取直链
String fileurl = asJson(res4).getJsonObject("data").getString("url");
String fileurl = res4.bodyAsJsonObject().getJsonObject("data").getString("url");
// 调试输出文件直链
System.out.println("文件直链: " + fileurl);
if (!fileurl.equals("")) {
promise.complete(URLDecoder.decode(fileurl, StandardCharsets.UTF_8));
} else {
if (!fileurl.equals(""))
{
try {
promise.complete(URLDecoder.decode(fileurl, "UTF-8"));
} catch (UnsupportedEncodingException e) {
promise.complete(fileurl);
}
}
else
{
this.fail("文件已失效");
}
@@ -148,7 +166,7 @@ public class WsTool extends PanBase {
this.fail("HTTP状态不正确可能是分享链接的方式已更新");
}
}).onFailure(this.handleFail(key));
}).onFailure(this.handleFail(this.key));
} catch (DecodeException | NullPointerException e) {
this.fail("token获取失败可能是分享链接的方式已更新");
@@ -157,7 +175,7 @@ public class WsTool extends PanBase {
this.fail("HTTP状态不正确可能是分享链接的方式已更新");
}
}).onFailure(this.handleFail(key));
}).onFailure(this.handleFail(this.key));
return promise.future();
}

View File

@@ -1,88 +1,51 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter;
import cn.qaiu.util.JsExecUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.pointer.JsonPointer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cn.qaiu.util.RandomStringGenerator.gen36String;
/**
* 123网盘
*/
public class YeTool extends PanBase {
public class YeTool extends PanBase implements IPanTool {
public static final String SHARE_URL_PREFIX = "https://www.123pan.com/s/";
public static final String FIRST_REQUEST_URL = SHARE_URL_PREFIX + "{key}.html";
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={ParentFileId}&Page=1&event=homeListFile&operateType=1";
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId=0&Page=1&event=homeListFile&operateType=1";
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/a/api/share/download/info?{authK}={authV}";
private static final String BATCH_DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/batch_download_share_info?{authK}={authV}";
private final MultiMap header = MultiMap.caseInsensitiveMultiMap();
public YeTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
header.set("App-Version", "3");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
//header.set("DNT", "1");
//header.set("Host", "www.123pan.com");
header.set("LoginUuid", gen36String());
header.set("Pragma", "no-cache");
header.set("Referer", shareLinkInfo.getStandardUrl());
header.set("Sec-Fetch-Dest", "empty");
header.set("Sec-Fetch-Mode", "cors");
header.set("Sec-Fetch-Site", "same-origin");
header.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0");
header.set("platform", "web");
header.set("sec-ch-ua", "\"Not)A;Brand\";v=\"99\", \"Microsoft Edge\";v=\"127\", \"Chromium\";v=\"127\"");
header.set("sec-ch-ua-mobile", "?0");
header.set("sec-ch-ua-platform", "Windows");
public YeTool(String key, String pwd) {
super(key, pwd);
}
public Future<String> parse() {
final String dataKey = shareLinkInfo.getShareKey();
final String pwd = shareLinkInfo.getSharePassword();
String dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key);
client.getAbs(UriTemplate.of(FIRST_REQUEST_URL)).setTemplateParam("key", dataKey).send().onSuccess(res -> {
String html = res.bodyAsString();
// 判断分享是否已经失效
if (html.contains("分享链接已失效")) {
fail("该分享已失效({})已失效", shareLinkInfo.getShareUrl());
return;
}
Pattern compile = Pattern.compile("window.g_initialProps\\s*=\\s*(.*);");
Matcher matcher = compile.matcher(html);
if (!matcher.find()) {
fail("该分享({})文件信息找不到, 可能分享已失效", shareLinkInfo.getShareUrl());
fail(html + "\n Ye: " + dataKey + " 正则匹配失败");
return;
}
String fileInfoString = matcher.group(1);
@@ -95,40 +58,24 @@ public class YeTool extends PanBase {
return;
}
String shareKey = resJson.getJsonObject("data").getString("ShareKey");
if (resListJson == null || resListJson.getInteger("code") != 0) {
// 加密分享
if (StringUtils.isNotEmpty(pwd)) {
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")
.send().onSuccess(res2 -> {
JsonObject infoJson = asJson(res2);
JsonObject infoJson = res2.bodyAsJsonObject();
if (infoJson.getInteger("code") != 0) {
fail("{} 状态码异常 {}", dataKey, infoJson);
return;
}
JsonObject getFileInfoJson =
infoJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
getFileInfoJson.put("ShareKey", shareKey);
// 判断是否为文件夹: data->InfoList->0->Type: 1为文件夹, 0为文件
try {
int type = (Integer)JsonPointer.from("/data/InfoList/0/Type").queryJson(infoJson);
if (type == 1) {
getZipDownUrl(client, getFileInfoJson);
return;
}
} catch (Exception exception) {
fail("该分享[{}]解析异常: {}", dataKey, exception.getMessage());
return;
}
getDownUrl(client, getFileInfoJson);
}).onFailure(this.handleFail(GET_FILE_INFO_URL));
} else {
@@ -139,11 +86,6 @@ public class YeTool extends PanBase {
JsonObject reqBodyJson = resListJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
reqBodyJson.put("ShareKey", shareKey);
if (reqBodyJson.getInteger("Type") == 1) {
// 文件夹
getZipDownUrl(client, reqBodyJson);
return;
}
getDownUrl(client, reqBodyJson);
}).onFailure(this.handleFail(FIRST_REQUEST_URL));
@@ -162,21 +104,6 @@ public class YeTool extends PanBase {
jsonObject.put("Etag", reqBodyJson.getString("Etag"));
// 调用JS文件获取签名
down(client, jsonObject, DOWNLOAD_API_URL);
}
private void getZipDownUrl(WebClient client, JsonObject reqBodyJson) {
log.info(reqBodyJson.encodePrettily());
JsonObject jsonObject = new JsonObject();
// {"ShareKey":"LH3rTd-1ENed","fileIdList":[{"fileId":17525952}]}
jsonObject.put("ShareKey", reqBodyJson.getString("ShareKey"));
jsonObject.put("fileIdList", new JsonArray().add(JsonObject.of("fileId", reqBodyJson.getInteger("FileId"))));
// 调用JS文件获取签名
down(client, jsonObject, BATCH_DOWNLOAD_API_URL);
}
private void down(WebClient client, JsonObject jsonObject, String api) {
ScriptObjectMirror getSign;
try {
getSign = JsExecUtils.executeJs("getSign", "/a/api/share/download/info");
@@ -186,13 +113,13 @@ public class YeTool extends PanBase {
}
log.info("ye getSign: {}={}", getSign.get("0").toString(), getSign.get("1").toString());
client.postAbs(UriTemplate.of(api))
client.postAbs(UriTemplate.of(DOWNLOAD_API_URL))
.setTemplateParam("authK", getSign.get("0").toString())
.setTemplateParam("authV", getSign.get("1").toString())
.putHeader("Platform", "web")
.putHeader("App-Version", "3")
.sendJsonObject(jsonObject).onSuccess(res2 -> {
JsonObject downURLJson = asJson(res2);
JsonObject downURLJson = res2.bodyAsJsonObject();
try {
if (downURLJson.getInteger("code") != 0) {
@@ -203,8 +130,7 @@ public class YeTool extends PanBase {
fail("Ye: downURLJson格式异常->" + downURLJson);
return;
}
String downURL = downURLJson.getJsonObject("data")
.getString(api.contains("batch_download_share_info")? "DownloadUrl" : "DownloadURL");
String downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
try {
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
String params = urlParams.get("params");
@@ -213,7 +139,7 @@ public class YeTool extends PanBase {
// 获取直链
client.getAbs(downUrl2).send().onSuccess(res3 -> {
JsonObject res3Json = asJson(res3);
JsonObject res3Json = res3.bodyAsJsonObject();
try {
if (res3Json.getInteger("code") != 0) {
fail("Ye: downUrl2返回值异常->" + res3Json);
@@ -233,121 +159,4 @@ 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

@@ -1,22 +1,19 @@
package cn.qaiu.util;
import io.vertx.core.net.impl.URIDecoder;
import cn.qaiu.util.jdk17halper.HexFormat;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Date;
import java.util.HexFormat;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* AES加解密工具类
@@ -42,12 +39,6 @@ public class AESUtils {
public static final String CIPHER_AES0;
public static final String CIPHER_AES0_IZ;
public static final String MG_PKEY2 = "D8jg+H2iNX94zvHhRLnSM3oy59dH2QQjxQ0GgKJSL+mJclbCcItjV3AmkPY6WcbV4hNQk5+hN2J1eTrxPQqF4p28e3FTsGRCXVN80CLS+XqpFNY/9xuyf2bvbeq5JJU1IBCXgSZmEo8zu0/VGS3YNeDsHtjg92QSrRY8i4A+shihZBSz0/0KOL1VPd/K4tAYvsI9YjVFOI7z9mJJ8Ek8rVUplurJyGkjevRfvReN7xQ67PR+yZovk72yTZKlHDz5jVpLGLOy2iwTTSTbTvtnOi9TSE6sSPtRHv16cxZYZQY=";
public static final String MG_PKEY;
public static final String MG_KEY = "4ea5c508a6566e76240543f8feb06fd457777be39549c4016436afda65d2330e";
/**
* 秘钥长度
*/
@@ -72,7 +63,6 @@ public class AESUtils {
try {
CIPHER_AES0 = decryptByBase64AES(CIPHER_AES2, CIPHER_AES);
CIPHER_AES0_IZ = decryptByBase64AES(CIPHER_AES2_IZ, CIPHER_AES);
MG_PKEY = decryptByBase64AES(MG_PKEY2, CIPHER_AES2);
} catch (IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException |
InvalidKeyException e) {
throw new RuntimeException(e);
@@ -339,73 +329,4 @@ public class AESUtils {
return _0x53928f + "-" + _0x430930 + "-" + _0x49ec94;
}
public static String encrypt(String str, String pwd) {
if (pwd == null || pwd.length() <= 0) {
throw new IllegalArgumentException("Please enter a password with which to encrypt the message.");
}
// 生成 prand 值
StringBuilder prand = new StringBuilder();
for (int i = 0; i < pwd.length(); i++) {
prand.append((int) pwd.charAt(i));
}
// 计算 sPos, mult, incr, modu
int sPos = prand.length() / 5;
long mult = Long.parseLong(prand.substring(sPos, sPos + 1) +
prand.substring(sPos * 2, sPos * 2 + 1) +
prand.substring(sPos * 3, sPos * 3 + 1) +
prand.substring(sPos * 4, sPos * 4 + 1) +
prand.substring(sPos * 5, sPos * 5 + 1));
int incr = (int) Math.ceil(pwd.length() / 2.0);
long modu = (long) Math.pow(2, 31) - 1;
if (mult < 2) {
throw new IllegalArgumentException("Algorithm cannot find a suitable hash. Please choose a different password.");
}
// 生成 salt 并加到 prand 上
long salt = Math.round(Math.random() * 1000000000) % 100000000;
prand.append(salt);
// 使用 BigInteger 处理超过 Long 范围的 prand 值
BigInteger prandValue = new BigInteger(prand.toString());
while (prandValue.toString().length() > 10) {
prandValue = prandValue.divide(BigInteger.TEN).add(prandValue.remainder(BigInteger.TEN));
}
// 将 prand 转换为 long 进行后续处理
prandValue = prandValue.multiply(BigInteger.valueOf(mult)).add(BigInteger.valueOf(incr)).mod(BigInteger.valueOf(modu));
// 加密过程
StringBuilder encStr = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
// 将字符和异或的结果强制转换为 int
int encChr = (int) (str.charAt(i) ^ (int) ((prandValue.doubleValue() / modu) * 255));
String hexStr = Integer.toHexString(encChr);
if (hexStr.length() < 2) {
encStr.append("0");
}
encStr.append(hexStr);
prandValue = prandValue.multiply(BigInteger.valueOf(mult)).add(BigInteger.valueOf(incr)).mod(BigInteger.valueOf(modu));
}
// 将 salt 转换为 16 进制并追加到加密结果
String saltHex = Long.toHexString(salt);
while (saltHex.length() < 8) {
saltHex = "0" + saltHex;
}
encStr.append(saltHex);
return encStr.toString();
}
public static void main(String[] args) {
// 示例
String encrypted = encrypt("HelloWorld", "password123");
System.out.println("Encrypted String: " + encrypted);
}
}

View File

@@ -1,18 +0,0 @@
package cn.qaiu.util;
/**
* 转换为任意类型 旨在消除泛型转换时的异常
*/
public interface CastUtil {
/**
* 泛型转换
* @param object 要转换的object
* @param <T> T
* @return T
*/
@SuppressWarnings("unchecked")
static <T> T cast(Object object) {
return (T) object;
}
}

View File

@@ -1,44 +0,0 @@
package cn.qaiu.util;
public class FileSizeConverter {
public static long convertToBytes(String sizeStr) {
if (sizeStr == null || sizeStr.isEmpty()) {
throw new IllegalArgumentException("Invalid file size string");
}
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) {
case 'B' -> (long) size;
case 'K' -> (long) (size * 1024);
case 'M' -> (long) (size * 1024 * 1024);
case 'G' -> (long) (size * 1024 * 1024 * 1024);
default -> throw new IllegalArgumentException("Unknown file size unit: " + unit);
};
}
public static String convertToReadableSize(long bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
return String.format("%.1f K", bytes / 1024.0);
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.1f M", bytes / (1024.0 * 1024));
} else {
return String.format("%.1f G", bytes / (1024.0 * 1024 * 1024));
}
}
}

View File

@@ -1,35 +0,0 @@
package cn.qaiu.util;
import io.vertx.core.MultiMap;
public class HeaderUtils {
/**
* 将请求头字符串转换为Vert.x的MultiMap对象
*
* @param headerString 请求头字符串
* @return MultiMap对象
*/
public static MultiMap parseHeaders(String headerString) {
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
if (headerString == null || headerString.isEmpty()) {
return headers;
}
// 按行分割字符串
String[] lines = headerString.split("\n");
for (String line : lines) {
// 按冒号分割键值对
String[] parts = line.split(":", 2);
if (parts.length == 2) {
String key = parts[0].trim();
String value = parts[1].trim();
headers.add(key, value);
}
}
return headers;
}
}

View File

@@ -1,80 +0,0 @@
package cn.qaiu.util;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.http.impl.headers.HeadersMultiMap;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientSession;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.List;
public class IpExtractor {
public static void main(String[] args) throws InterruptedException {
// 创建请求头Map
MultiMap headers = new HeadersMultiMap();
headers.add("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
headers.add("accept-language", "zh-CN,zh;q=0.9,en;q=0.8");
headers.add("cache-control", "no-cache");
headers.add("dnt", "1");
headers.add("origin", "https://ip.ihuan.me");
headers.add("pragma", "no-cache");
headers.add("priority", "u=0, i");
headers.add("referer", "https://ip.ihuan.me/ti.html");
headers.add("sec-ch-ua", "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"");
headers.add("sec-ch-ua-mobile", "?0");
headers.add("sec-ch-ua-platform", "\"Windows\"");
headers.add("sec-fetch-dest", "document");
headers.add("sec-fetch-mode", "navigate");
headers.add("sec-fetch-site", "same-origin");
headers.add("sec-fetch-user", "?1");
headers.add("upgrade-insecure-requests", "1");
headers.add("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36");
headers.add("Content-Type", "application/x-www-form-urlencoded");
WebClient client = WebClient.create(Vertx.vertx());
WebClientSession webClientSession = WebClientSession.create(client);
webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res->{
System.out.println(res.toString());
webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res2->{
System.out.println(res2.toString());
});
});
//
// String htmlContent = "<div class=\"panel-heading\">提取结果</div>\n" +
// "<div class=\"panel-body\">111.1.27.85:80<br>42.63.65.46:80<br>118.195.242.20:8080<br>42.63.65.119:80<br>117.50.108.90:7890<br>116.62.50.250:7890<br>114.231.8.177:8089<br>190.43.92.90:999<br>221.178.85.68:8080<br><br>61.160.202.86:80<br>42.63.65.86:80<br>42.63.65.7:80<br>42.63.65.41:80<br>159.226.227.119:80<br>61.160.202.52:80<br>42.63.65.15:80<br>112.17.16.204:80<br>61.160.202.53:80<br>42.63.65.9:80<br>42.63.65.60:80<br>42.63.65.18:80<br>203.190.115.177:8071<br>42.63.65.38:80<br>42.63.65.31:80<br>91.185.3.126:8080<br>139.9.119.20:80<br>1.15.47.213:443<br>183.164.243.108:8089<br>165.225.208.177:80<br>194.163.132.232:3128<br>91.235.220.122:80<br>39.100.120.200:7890<br>141.147.33.121:80<br>183.164.243.138:8089<br>104.129.205.94:54321<br>117.160.250.138:81<br>180.120.213.208:8089<br>61.130.9.37:443<br>182.34.18.206:9999<br>117.86.12.150:8089<br>27.192.173.108:9000<br>183.164.243.11:8089<br>114.231.41.205:8089<br>103.104.233.78:8080<br>183.164.243.174:8089<br>36.6.144.230:8089<br>111.224.213.239:8089<br>182.92.73.106:80<br>36.6.145.81:8089<br>117.69.232.125:8089<br>36.6.144.64:8089<br>117.57.92.20:8089<br>47.100.69.29:8888<br>117.57.93.246:8089<br>120.234.203.171:9002<br>114.231.46.231:8089<br>183.164.242.199:8089<br>117.69.237.179:8089<br>182.44.32.239:7890<br>47.100.91.57:8080<br>117.69.236.127:8089<br>114.231.8.18:8089<br>117.69.232.183:8089<br>117.69.237.29:8089<br>183.164.242.67:8089<br>183.164.242.35:8089<br>183.164.243.71:8089<br>113.223.214.155:8089<br>36.6.145.132:8089<br>182.106.220.252:9091<br>113.223.212.176:8089<br>62.152.53.186:8909<br>117.57.92.16:8089<br>183.164.243.186:8089<br>36.6.144.210:8089<br>183.164.242.189:8089<br>213.178.39.170:8080<br>121.52.145.163:8080<br>36.6.144.240:8089<br>60.188.5.234:80<br>113.223.213.48:8089<br>183.164.243.149:8089<br>200.58.87.195:8080<br>36.6.144.153:8089<br>36.6.144.67:8089<br>36.6.145.182:8089<br>117.57.93.226:8089<br>42.112.24.127:8888<br>43.138.20.156:80<br>117.57.92.79:8089<br>65.109.111.238:3128<br>183.166.137.201:41122<br>113.223.213.150:8089<br>36.6.145.154:8089<br>185.5.209.101:80<br>36.6.144.17:8089<br>114.231.8.244:8089<br>117.69.237.24:8089<br>117.69.236.232:8089<br>117.69.236.127:8089<br>114.231.8.18:8089<br>117.69.232.183:8089<br>117.69.237.29:8089<br>183.164.242.67:8089<br>183.164.242.35:8089<br>183.164.243.71:8089<br>113.223.214.155:8089<br>36.6.145.132:8089<br>182.106.220.252:9091<br>113.223.212.176:8089<br>62.152.53.186:8909<br>117.57.92.16:8089<br>183.164.243.186:8089<br>36.6.144.210:8089<br>183.164.242.189:8089<br>213.178.39.170:8080<br>121.52.145.163:8080<br>36.6.144.240:8089<br>60.188.5.234:80<br>113.223.213.48:8089<br>183.164.243.149:8089<br>200.58.87.195:8080<br>36.6.144.153:8089<br>36.6.144.67:8089<br>36.6.145.182:8089<br>117.57.93.226:8089<br>42.112.24.127:8888<br>43.138.20.156:80<br>117.57.92.79:8089<br>65.109.111.238:3128<br>183.166.137.201:41122<br>113.223.213.150:8089<br>36.6.145.154:8089<br>185.5.209.101:80<br>36.6.144.17:8089<br>114.231.8.244:8089<br>117.69.237.24:8089<br>117.69.236.232:8089</div>";
//
// // 正则表达式匹配提取结果关键字下面的IP地址
// Pattern pattern = Pattern.compile("<div class=\"panel-heading\">提取结果</div>\\s*<div class=\"panel-body\">([\\s\\S]*?)(?=</div>)", Pattern.DOTALL);
// Matcher matcher = pattern.matcher(htmlContent);
//
// if (matcher.find()) {
// String ipText = matcher.group(1); // 获取匹配的IP地址部分
// Pattern ipPattern = Pattern.compile("(?:[0-9]{1,3}\\.){3}[0-9]{1,3}(?::\\d+)?");
// Matcher ipMatcher = ipPattern.matcher(ipText);
//
// List<String> ips = new ArrayList<>();
// while (ipMatcher.find()) {
// ips.add(ipMatcher.group());
// }
//
// System.out.println("提取到的IP地址");
// for (String ip : ips) {
//// System.out.println(ip);
// }
// } else {
// System.out.println("没有找到匹配的IP地址");
// }
//
// TimeUnit.SECONDS.sleep(1000);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,6 @@ import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import static cn.qaiu.util.AESUtils.encrypt;
/**
* 执行Js脚本
*
@@ -62,49 +57,4 @@ public class JsExecUtils {
}
/**
* 调用执行蓝奏云js文件
*/
public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException,
NoSuchMethodException {
ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
engine.eval(jsText);
Invocable inv = (Invocable) engine;
//调用js中的函数
if (StringUtils.isNotEmpty(funName)) {
return inv.invokeFunction(funName, args);
}
throw new ScriptException("funName is null");
}
public static String getKwSign(String s, String pwd) {
try {
return executeOtherJs(JsContent.kwSignString, "encrypt", s, pwd).toString();
} catch (ScriptException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
public static String mgEncData(String data, String key) {
try {
return executeOtherJs(JsContent.mgJS, "enc", data, key).toString();
} catch (ScriptException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
// return OM.AES.encrypt('{"copyrightId":"6326951FKBL","type":1,"auditionsFlag":0}',
// '4ea5c508a6566e76240543f8feb06fd457777be39549c4016436afda65d2330e').toString()
//
public static void main(String[] args) {
System.out.println(URLEncoder
.encode(mgEncData("{\"copyrightId\":\"6326951FKBL\",\"type\":1,\"auditionsFlag\":0}", AESUtils.MG_KEY), StandardCharsets.UTF_8));
// U2FsdGVkX1/UiZC91ImQvQY7qDBSEbTykAcVoARiquibPCZhWSs3kWQw3j2PNme5wNLqt2oau498ni1hgjGFuxwORnkk6x9rzk/X0arElUo=
}
}

View File

@@ -1,12 +0,0 @@
package cn.qaiu.util;
/**
* @author <a href="https://qaiu.top">QAIU</a>
* @date 2023/7/16 1:53
*/
public class PanExceptionUtils {
public static RuntimeException fillRunTimeException(String name, String dataKey, Throwable t) {
return new RuntimeException(name + ": 请求异常: key = " + dataKey, t.fillInStackTrace());
}
}

View File

@@ -1,29 +0,0 @@
package cn.qaiu.util;
import java.security.SecureRandom;
import java.util.UUID;
public class RandomStringGenerator {
private static final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789";
private static final int LENGTH = 13; // 每段长度为13
public static String generateRandomString() {
SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 2; i++) { // 生成两段
for (int j = 0; j < LENGTH; j++) {
int index = random.nextInt(CHARACTERS.length());
sb.append(CHARACTERS.charAt(index));
}
}
return sb.toString();
}
public static String gen36String() {
String uuid = UUID.randomUUID().toString().toLowerCase();
// 移除短横线
return uuid.replace("-", "");
}
}

View File

@@ -1,74 +0,0 @@
package cn.qaiu.util;
import io.vertx.core.AsyncResult;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.impl.headers.HeadersMultiMap;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientSession;
public class ReqIpUtil {
public static String BASE_URL = "https://ip.ihuan.me";
public static String BASE_URL_TEMPLATE = BASE_URL + "/{path}";
// GET https://ip.ihuan.me/mouse.do -> $("input[name='key']").val("30b4975b5547fed806bd2b9caa18485a");
public static String PATH1 = "mouse.do";
public static String PATH2 = "tqdl.html";
// 创建请求头Map
static MultiMap headers = new HeadersMultiMap();
static {
headers.set("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
headers.set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8");
headers.set("cache-control", "no-cache");
headers.set("dnt", "1");
headers.set("origin", "https://ip.ihuan.me");
headers.set("pragma", "no-cache");
headers.set("priority", "u=0, i");
headers.set("referer", "https://ip.ihuan.me");
headers.set("sec-ch-ua", "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"");
headers.set("sec-ch-ua-mobile", "?0");
headers.set("sec-ch-ua-platform", "\"Windows\"");
headers.set("sec-fetch-dest", "document");
headers.set("sec-fetch-mode", "navigate");
headers.set("sec-fetch-site", "same-origin");
headers.set("sec-fetch-user", "?1");
headers.set("upgrade-insecure-requests", "1");
// headers.set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36");
}
Vertx vertx = Vertx.vertx();
WebClient webClient = WebClient.create(vertx);
// 发送GET请求
WebClientSession webClientSession = WebClientSession.create(webClient);
public void exec() {
webClientSession.getAbs(BASE_URL)
.putHeaders(headers) // 将请求头Map添加到请求中
.send(this::next);
}
void next(AsyncResult<HttpResponse<Buffer>> response) {
if (response.failed()) {
response.cause().printStackTrace();
} else {
HttpResponse<Buffer> res = response.result();
System.out.println("Received response with status code " + res.statusCode());
System.out.println("Body: " + res.body());
webClientSession.getAbs(BASE_URL_TEMPLATE).setTemplateParam("path", PATH1)
.putHeaders(headers) // 将请求头Map添加到请求中
.send(response2 -> {
System.out.println(response2.result().bodyAsString());
});
}
}
}

View File

@@ -1,47 +0,0 @@
package cn.qaiu.util;
import org.apache.commons.lang3.StringUtils;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class URLUtil {
private final Map<String, String> queryParams = new HashMap<>();
// 构造函数传入URL并解析参数
private URLUtil(String url) {
try {
URL parsedUrl = new URL(url);
String ref = parsedUrl.getRef();
if (StringUtils.isNotEmpty(ref)) {
parsedUrl = new URL(parsedUrl.getProtocol() + "://" + parsedUrl.getHost() + ref);
}
String query = parsedUrl.getQuery();
if (query != null) {
String[] pairs = query.split("&");
for (String pair : pairs) {
String[] keyValue = pair.split("=");
String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8) : "";
queryParams.put(key, value);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 静态方法用于创建UrlUtil实例
public static URLUtil from(String url) {
return new URLUtil(url);
}
// 获取参数的方法
public String getParam(String param) {
return queryParams.get(param);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -54,7 +54,6 @@
<logger name="io.netty" level="warn"/>
<logger name="io.vertx" level="info"/>
<logger name="com.zaxxer.hikari" level="info"/>
<logger name="cn.qaiu" level="debug"/>
<root level="info">
<appender-ref ref="STDOUT"/>
<!-- <appender-ref ref="FILE"/>-->

View File

@@ -1,64 +0,0 @@
import requests
import urllib.parse
import re
import base64
"""
https://github.com/chenhal/short_url
"""
headers = {
'Cookie': 'SUB=',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
'Referer': 'https://www.weibo.com',
'Content-Type': 'application/x-www-form-urlencoded'
}
def get_short_url(long_url):
url = "https://www.weibo.com/aj/v6/comment/add"
payload = urllib.parse.urlencode({
'mid': '5094736413852129',
'content': long_url
})
response = requests.post(url, headers=headers, data=payload)
try:
# print(response.json())
data = response.json()['data']['comment']
short_url = re.search(r'(https?)://t.cn/\w+', data).group(0)
comment_id = re.findall(r'comment_id="(.+\d)"', data)[-1] # 评论id
print('微博短链:' + short_url)
del_comment(comment_id) # 需要删除评论,可以取消该行注释
except:
print('失败')
pass
# 删除评论
def del_comment(comment_id):
url = 'https://www.weibo.com/aj/comment/del'
payload = urllib.parse.urlencode({
'mid': '微博mid',
'cid': comment_id # 评论id
})
response = requests.post(url, headers=headers, data=payload)
try:
if response.json()['code'] == '100000':
print('评论已删除')
except:
pass
if __name__ == '__main__':
# https://so.toutiao.com/search/jump?url=https%3A%2F%2Fblog.qaiu.top%2Farchives%2Fpydroidall&aid=4916&jtoken=297f06127cb010274213422b1967bdc2ae8469b627205941dc287173b58a2a8439ea0d813d24ada8780047d33f37d7e82c6a620760de1ca37640c1dc143b4e01
# https://so.toutiao.com/search/jump?url=https%3A%2F%2Fblog.qaiu.top%2Farchives%2Fpydroidall1&aid=4916&jtoken=297f06127cb010274213422b1967bdc2ae8469b627205941dc287173b58a2a8439ea0d813d24ada8780047d33f37d7e82c6a620760de1ca37640c1dc143b4e01
# 原始 URL
url = "https://lz.qaiu.top/json/lz/i5vOm0xho2cj"
# 将 URL 编码为 Base64
encoded_url = base64.b64encode(url.encode("utf-8")).decode("utf-8")
get_short_url('https://www.so.com/link?m=ewgUSYiFWXIoTybC3fJH8YoJy8y10iRquo6cazgINwWjTn3HvVJ92TrCJu0PmMUR0RMDfOAucP3wa4G8j64SrhNH9Z0Cr0PEyn9ASuvpkUGmAjjUEGJkO5%2BIDGWVrEkPHsL7UsoKO6%2BlT%2BD6r&ccc=' + encoded_url)

View File

@@ -1,37 +0,0 @@
package cn.qaiu.parser;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FCURLParser {// 定义前缀
public static final String SHARE_URL_PREFIX0 = "https://v2.fangcloud.com/s";
public static final String SHARE_URL_PREFIX = "https://v2.fangcloud.com/sharing/";
public static final String SHARE_URL_PREFIX2 = "https://v2.fangcloud.cn/sharing/";
// 定义正则表达式,适用于所有前缀
private static final String SHARING_REGEX = "https://www\\.ecpan\\.cn/web(/%23|/#)?/yunpanProxy\\?path=.*&data=" +
"([^&]+)&isShare=1";
public static void main(String[] args) {
// 测试 URL
String[] urls = {
"https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data=4b3d786755688b85c6eb0c04b9124f4dalzdaJpXHx&isShare=1",
"https://www.ecpan.cn/web/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data=4b3d786755688b85c6eb0c04b9124f4dalzdaJpXHx&isShare=1",
"https://v2.fangcloud.cn/sharing/xyz789"
};
// 编译正则表达式
Pattern pattern = Pattern.compile(SHARING_REGEX);
for (String url : urls) {
Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
System.out.println(matcher.groupCount());
String shareKey = matcher.group(matcher.groupCount()); // 捕捉组 3
System.out.println("Captured part: " + shareKey);
} else {
System.out.println("No match found.");
}
}
}
}

View File

@@ -1,106 +0,0 @@
package cn.qaiu.parser;
import cn.qaiu.entity.ShareLinkInfo;
import org.junit.Test;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import static java.util.regex.Pattern.compile;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* @author <a href="https://qaiu.top">QAIU</a>
* @date 2024/8/8 2:39
*/
public class PanDomainTemplateTest {
@Test
public void normalizeShareLink() {
// 准备测试数据
String testShareUrl = "https://test.lanzoux.com/s/someShareKey";
ParserCreate parserCreate = ParserCreate.fromShareUrl(testShareUrl); // 假设使用LZ网盘模板
// 调用normalizeShareLink方法
ShareLinkInfo result = parserCreate.getShareLinkInfo();
System.out.println(result);
// 断言结果是否符合预期
assertNotNull("Result should not be null", result);
assertEquals("Share key should match", "someShareKey", result.getShareKey());
assertEquals("Standard URL should be generated correctly", parserCreate.getStandardUrlTemplate().replace("{shareKey}", "someShareKey"), result.getStandardUrl());
// 可以添加更多的断言来验证其他字段
}
@Test
public void fromShareUrl() throws InterruptedException {
// 准备测试数据
String lzUrl = "https://wwn.lanzouy.com/ihLkw1gezutg";
String cowUrl = "https://cowtransfer.com/s/9a644fe3e3a748";
String ceUrl = "https://pan.huang1111.cn/s/g31PcQ";
String wsUrl = "https://f.ws59.cn/f/f25625rv6p6";
// ParserCreate.fromShareUrl(wsUrl).createTool()
// .parse().onSuccess(System.out::println);
// ParserCreate.fromShareUrl(lzUrl).createTool()
// .parse().onSuccess(System.out::println);
// ParserCreate.fromShareUrl(cowUrl).createTool()
// .parse().onSuccess(System.out::println);
ParserCreate.fromShareUrl(lzUrl).createTool()
.parse().onSuccess(System.out::println);
// ParserCreate.fromType("lz").shareKey("ihLkw1gezutg")
// .createTool().parse().onSuccess(System.out::println);
// ParserCreate.LZ.shareKey("ihLkw1gezutg")
// .createTool().parse().onSuccess(System.out::println);
// 调用fromShareUrl方法
// PanDomainTemplate resultTemplate = ParserCreate.fromShareUrl(testShareUrl);
// System.out.println(resultTemplate.normalizeShareLink(testShareUrl));
// System.out.println(resultTemplate.shareKey("xxx"));
// System.out.println(resultTemplate.createTool("xxx",null).parse()
// .onSuccess(System.out::println));
// System.out.println(resultTemplate.getDisplayName());
// System.out.println(resultTemplate.getStandardUrlTemplate());
// System.out.println(resultTemplate.getRegexPattern());
//
// // 断言结果是否符合预期
// assertNotNull("Result should not be null", resultTemplate);
// assertEquals("Should return the correct template", ParserCreate.LZ, resultTemplate);
// // 可以添加更多的断言来验证正则表达式匹配逻辑
// new Scanner(System.in).nextLine();
TimeUnit.SECONDS.sleep(5);
}
@Test
public void verifyDuplicates() {
Matcher matcher = compile("https://(?:[a-zA-Z\\d-]+\\.)?drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?")
.matcher("https://adsd.drive.google.com/file/d/151bR-nk-tOBm9QAFaozJIVt2WYyCMkoz/view");
if (matcher.find()) {
System.out.println(matcher.group());
System.out.println(matcher.group("KEY"));
}
// 校验重复
Set<String> collect =
Arrays.stream(PanDomainTemplate.values()).map(PanDomainTemplate::getRegex).collect(Collectors.toSet());
if (collect.size()<PanDomainTemplate.values().length) {
System.out.println("有重复枚举正则");
} else {
System.out.println("正则无重复");
}
Set<String> collect2 =
Arrays.stream(PanDomainTemplate.values()).map(PanDomainTemplate::getStandardUrlTemplate).collect(Collectors.toSet());
if (collect2.size()<PanDomainTemplate.values().length) {
System.out.println("有重复枚举标准链接");
} else {
System.out.println("标准链接无重复");
}
}
}

View File

@@ -1,28 +0,0 @@
package cn.qaiu.parser;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.impl.PodTool;
public class ParserUrlOut {
//https://onedrive.live.com/redir?resid=ABFD0A26E47D3458!4699&e=OggA4s&migratedtospo=true&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3UvcyFBbGcwZmVRbUN2MnJwRnZ1NDQ0aGc1eVZxRGNLP2U9T2dnQTRz
public static void main(String[] args) {
// Matcher matcher = redirectUrlRegex.matcher("https://onedrive.live.com/redir?resid=ABFD0A26E47D3458!4698" +
// "&authkey=!ACpvXghP5xhG_cg&e=hV98W1");
// if (matcher.find()) {
// System.out.println(matcher.group("cid"));
// System.out.println(matcher.group("cid2"));
// System.out.println(matcher.group("authkey"));
// }
// appid 5cbed6ac-a083-4e14-b191-b4ba07653de2 5cbed6ac-a083-4e14-b191-b4ba07653de2
// https://my.microsoftpersonalcontent.com/personal/abfd0a26e47d3458/_layouts/15/embed.aspx?UniqueId=e47d3458-0a26-20fd-80ab-5b1200000000&Translate=false&ApiVersion=2.0
// https://my.microsoftpersonalcontent.com/personal/abfd0a26e47d3458/_layouts/15/embed.aspx?UniqueId=6b0900d6-abcf-44ce-b7bf-7b626bcbe4b8&Translate=false&ApiVersion=2.0
// https://1drv.ms/u/s!Alg0feQmCv2rpFvu444hg5yVqDcK?e=OggA4s
// https://1drv.ms/u/c/abfd0a26e47d3458/EVg0feQmCv0ggKtbEgAAAAABqGv8K6HmOwLRsvokyV5fUg?e=iqoRc0
// https://1drv.ms/u/c/abfd0a26e47d3458/EVg0feQmCv0ggKtaEgAAAAAB-lF1qjkfv5OqdrT9VSMDMw
new PodTool(ShareLinkInfo.newBuilder().shareUrl("https://1drv.ms/u/c/abfd0a26e47d3458/EdYACWvPq85Et797YmvL5LgBruUKoNxqIFATXhIv1PI2_Q")
.build())
.parse().onSuccess(System.out::println);
}
}

View File

@@ -1,32 +0,0 @@
package cn.qaiu.util;
import org.junit.Test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TestRegex {
@Test
public void regexYFC() {
String html = """
true|
<div class="load" id="go"><a
href="https://nsub2d3.118pan.com/dl.php?YzljNGFnOHFabWpsdzVXNUhyOXNiQ0ZYMHEzUjFoQ0R6MExCckdKZmRQL1RXbERPSTUwS0xlZkUvdmN5dGNqTHhCNDJVZURFekthV0tROHNINXdNRkZYUXB5aGQ5TEdXeTgrNkc1TkZwOWhzYVdpak83dEZleWxpYnloc0c1Ky9wbVBSZ0xiSHo5aTBnVVpCRmpyMGFBRERjcldtc0FnbS9vMnd3ZGRHWU4zYTNzNncwbk56UEtOVWhFeGIvWlM1aWZLQjlaTjB3b2V0cGVrb2lpQ0x3U0dtQkJqd2plWk1yeU1mZG1oNXpRYUJpYmFhZVgvWFZScThsTFhmU1ZKRW9UT3haVHFMVGVRTUhR"
onclick="down_process2('1228264');" target="_blank"><span class="txt">极速下载</span></a><a
href="https://nsub2t3.118pan.com/dl.php?YzljNGFnOHFabWpsdzVXNUhyOXNiQ0ZYMHEzUjFoQ0R6MExCckdKZmRQL1RXbERPSTUwS0xlZkUvdmN5dGNqTHhCNDJVZURFekthV0tROHNINXdNRkZYUXB5aGQ5TEdXeTgrNkc1TkZwOWhzYVdpak83dEZleWxpYnloc0c1Ky9wbVBSZ0xiSHo5aTBnVVpCRmpyMGFBRERjcldtc0FnbS9vMnd3ZGRHWU4zYTNzNncwbk56UEtOVWhFeGIvWlM1aWZLQjlaTjB3b2V0cGVrb2lpQ0x3U0dtQkJqd2plWk1yeU1mZG1oNXpRYUJpYmFhZVgvWFZScThsTFhmU1ZKRW9UT3haVHFMVGVRTUhR"
onclick="down_process2('1228264');" target="_blank"><span class="txt txtc">电信下载</span></a><a
href="https://nsub2u3.118pan.com/dl.php?YzljNGFnOHFabWpsdzVXNUhyOXNiQ0ZYMHEzUjFoQ0R6MExCckdKZmRQL1RXbERPSTUwS0xlZkUvdmN5dGNqTHhCNDJVZURFekthV0tROHNINXdNRkZYUXB5aGQ5TEdXeTgrNkc1TkZwOWhzYVdpak83dEZleWxpYnloc0c1Ky9wbVBSZ0xiSHo5aTBnVVpCRmpyMGFBRERjcldtc0FnbS9vMnd3ZGRHWU4zYTNzNncwbk56UEtOVWhFeGIvWlM1aWZLQjlaTjB3b2V0cGVrb2lpQ0x3U0dtQkJqd2plWk1yeU1mZG1oNXpRYUJpYmFhZVgvWFZScThsTFhmU1ZKRW9UT3haVHFMVGVRTUhR"
onclick="down_process2('1228264');" target="_blank"><span class="txt">联通下载</span></a><a
href="https://nsub2d3.118pan.com/dl.php?YzljNGFnOHFabWpsdzVXNUhyOXNiQ0ZYMHEzUjFoQ0R6MExCckdKZmRQL1RXbERPSTUwS0xlZkUvdmN5dGNqTHhCNDJVZURFekthV0tROHNINXdNRkZYUXB5aGQ5TEdXeTgrNkc1TkZwOWhzYVdpak83dEZleWxpYnloc0c1Ky9wbVBSZ0xiSHo5aTBnVVpCRmpyMGFBRERjcldtc0FnbS9vMnd3ZGRHWU4zYTNzNncwbk56UEtOVWhFeGIvWlM1aWZLQjlaTjB3b2V0cGVrb2lpQ0x3U0dtQkJqd2plWk1yeU1mZG1oNXpRYUJpYmFhZVgvWFZScThsTFhmU1ZKRW9UT3haVHFMVGVRTUhR"
onclick="down_process2('1228264');" target="_blank"><span class="txt txtc">普通下载</span></a></div>
""";
Pattern compile = Pattern.compile("href=\"([^\"]+)\"");
Matcher matcher = compile.matcher(html);
if (matcher.find()) {
// System.out.println(matcher.group(0));
System.out.println(matcher.group(1));
}
}
}

View File

@@ -1,19 +0,0 @@
package cn.qaiu.util;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.*;
public class URLUtilTest {
@Test
public void getParam() {
URLUtil util = URLUtil.from("https://i.y.qq.com/v8/playsong.html?ADTAG=cbshare&_wv=1&appshare=android_qq" +
"&appsongtype=1&appversion=13100008&channelId=10036163&hosteuin=7iosow-s7enz&openinqqmusic=1&platform" +
"=11&songmid=000XjcLg0fbRjv&type=0");
Assert.assertEquals(util.getParam("songmid"), "000XjcLg0fbRjv");
Assert.assertEquals(util.getParam("type"), "0");
}
}

Some files were not shown because too many files have changed in this diff Show More