Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
579c35fadc | ||
|
|
72506d6b5f | ||
|
|
f3a6d665cf | ||
|
|
41201653f1 | ||
|
|
39cac1bacb | ||
|
|
55e338f35c | ||
|
|
f7fe5d68e7 | ||
|
|
6fc0915117 | ||
|
|
000618ac5e | ||
|
|
66f39ea9e2 | ||
|
|
ef5c6e4644 | ||
|
|
9fe3863c31 | ||
|
|
7679bbab38 | ||
|
|
365f3de136 | ||
|
|
dbc965c6fe | ||
|
|
75ccf228cd | ||
|
|
e2a6238ab9 | ||
|
|
98e53b38db | ||
|
|
846bf0345a | ||
|
|
95ddc95c79 | ||
|
|
956105c16e | ||
|
|
3b9ee5eb96 | ||
|
|
a03b57cbb0 | ||
|
|
2c2aa50a88 | ||
|
|
5cc955f590 | ||
|
|
33215957bf | ||
|
|
473ac0d468 | ||
|
|
0f6b6839c4 | ||
|
|
e67d95a82b | ||
|
|
edbc4c50c9 | ||
|
|
119bd3a516 | ||
|
|
1fad4d7137 | ||
|
|
6f9b009194 | ||
|
|
de37c26423 | ||
|
|
e975b2822b | ||
|
|
0a361e974d | ||
|
|
70176a46a1 | ||
|
|
36e4b3273d | ||
|
|
282cb70cf5 | ||
|
|
195524f2ee | ||
|
|
759e6a451b | ||
|
|
d0c9a78067 | ||
|
|
518037cee8 | ||
|
|
b153b2aaf6 | ||
|
|
46ec89d201 | ||
|
|
b06fc18062 | ||
|
|
5809871cf1 | ||
|
|
f6b7ecdc83 | ||
|
|
53a2b04e60 | ||
|
|
8e27444f0e | ||
|
|
59e024fd40 | ||
|
|
ba9d3c7826 | ||
|
|
44b3920055 | ||
|
|
a939c233dc | ||
|
|
cd3964a8f8 | ||
|
|
cbd00b2fcf | ||
|
|
2fe35a4ebb | ||
|
|
5b3fca3fdc | ||
|
|
e6b8963069 | ||
|
|
e55433c3f8 | ||
|
|
a45663b1f1 | ||
|
|
4646e7db78 | ||
|
|
6d924efba2 | ||
|
|
0efded719f | ||
|
|
b20a29ab8c | ||
|
|
aa848bf63f | ||
|
|
6e79107070 | ||
|
|
c18f544c26 | ||
|
|
52d4216727 | ||
|
|
4ece1ec80a | ||
|
|
f692ce57ee | ||
|
|
4669935200 | ||
|
|
f005d4f614 | ||
|
|
a7e61cd937 | ||
|
|
202349b3a9 | ||
|
|
54a1c222c7 | ||
|
|
4c245d2c34 | ||
|
|
d2e63b96eb | ||
|
|
cc7603f92b | ||
|
|
c80c73d3cc | ||
|
|
c2f674cef0 | ||
|
|
3c193dcd74 | ||
|
|
7b4fd666b4 | ||
|
|
eea76d3aa3 | ||
|
|
69e4baee87 | ||
|
|
ef5f71b4db | ||
|
|
2bdf315f4b | ||
|
|
71b4fca6c2 | ||
|
|
b6aca2e2e9 | ||
|
|
996210f8c7 | ||
|
|
de6e4356a4 | ||
|
|
da7a5e93c8 | ||
|
|
546f4300a1 | ||
|
|
b724fbb98a | ||
|
|
9222a703b4 | ||
|
|
f62ca7a057 | ||
|
|
c3ff5a49bd | ||
|
|
a02ff884f5 | ||
|
|
668897d1df | ||
|
|
9866a9d93d | ||
|
|
9b9c5fe00a | ||
|
|
dc8362db08 | ||
|
|
bc2cd1504e | ||
|
|
b118231f58 | ||
|
|
c910a986b1 | ||
|
|
f8d10236e3 | ||
|
|
2f0c51283c | ||
|
|
8611824b9a | ||
|
|
2b16246beb | ||
|
|
62464fec17 | ||
|
|
92a86ce8e0 | ||
|
|
13f89b32c6 | ||
|
|
d9fc4659b8 | ||
|
|
8309f4a4d4 | ||
|
|
805f624b89 | ||
|
|
4b2d78a0b2 | ||
|
|
e1e0a6afc4 | ||
|
|
d5a802c218 | ||
|
|
70093a3f2c | ||
|
|
81d4098b6c | ||
|
|
6f976f242a | ||
|
|
50090db1f4 | ||
|
|
4225f1986b | ||
|
|
f398f3fa07 | ||
|
|
ffe95fcf66 | ||
|
|
83fd60f1a1 | ||
|
|
dda9ec0a01 | ||
|
|
b108d24981 | ||
|
|
90051b9aa0 | ||
|
|
dc3afeae1d | ||
|
|
c59ef3f0cf | ||
|
|
6649e14472 | ||
|
|
3394ae7400 | ||
|
|
fdb13e8257 | ||
|
|
13244e1dcf | ||
|
|
bc8167a724 | ||
|
|
b184e56eac | ||
|
|
219966826c | ||
|
|
c2d4550f85 | ||
|
|
f135012609 | ||
|
|
170fdbac44 | ||
|
|
fb9c405633 | ||
|
|
e797e04294 | ||
|
|
a8ae225c40 | ||
|
|
c0904105b4 | ||
|
|
5d961c7676 | ||
|
|
2c2e8e94d7 | ||
|
|
5e8ecd4052 | ||
|
|
3c4b92160d | ||
|
|
30a0d07b1a | ||
|
|
9593ec4811 | ||
|
|
a59af79423 | ||
|
|
7d1552eeca | ||
|
|
d05db559ab | ||
|
|
fd3d439b3e | ||
|
|
78af86a1ef | ||
|
|
9e50d52d0a | ||
|
|
6423ec1053 | ||
|
|
c8332049c1 | ||
|
|
33426f5fc8 | ||
|
|
f8e78506d0 | ||
|
|
7472a96282 | ||
|
|
a632d36ba2 | ||
|
|
f1cf1f0eb8 | ||
|
|
c90262485f | ||
|
|
7d8701db0a | ||
|
|
47d05dc37b | ||
|
|
c588312f81 | ||
|
|
b3e0eead61 | ||
|
|
09109fe9bf | ||
|
|
fdc4cbb2e8 | ||
|
|
95d1449651 | ||
|
|
b21c75e125 | ||
|
|
8b1f5067f2 | ||
|
|
9fd7b5934d | ||
|
|
53b4f94995 | ||
|
|
16866a00b9 | ||
|
|
7229388cdd | ||
|
|
138bfde587 | ||
|
|
8bc876646d | ||
|
|
a56565e187 | ||
|
|
925144ea79 | ||
|
|
0a47d48c60 | ||
|
|
749d1b7039 | ||
|
|
92df9239af | ||
|
|
7e612541a4 | ||
|
|
10030346d0 | ||
|
|
9d599b5e64 | ||
|
|
26060f0dd7 | ||
|
|
4e45e37412 | ||
|
|
280f0ef060 | ||
|
|
63230d5c2b | ||
|
|
c9c83cb65a | ||
|
|
c3c4ad6c00 | ||
|
|
9c5ade608e | ||
|
|
9da63f12c7 | ||
|
|
2427a6d26b | ||
|
|
a0681e5e44 |
15
Dockerfile
@ -1,6 +1,12 @@
|
|||||||
# 使用官方 Python 镜像作为基础镜像
|
# 使用官方 Python 镜像作为基础镜像
|
||||||
FROM python:3.13-alpine
|
FROM python:3.13-alpine
|
||||||
|
|
||||||
|
#构建版本
|
||||||
|
ARG BUILD_SHA
|
||||||
|
ARG BUILD_TAG
|
||||||
|
ENV BUILD_SHA=$BUILD_SHA
|
||||||
|
ENV BUILD_TAG=$BUILD_TAG
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -8,17 +14,12 @@ WORKDIR /app
|
|||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||||
|
echo "{\"BUILD_SHA\":\"$BUILD_SHA\", \"BUILD_TAG\":\"$BUILD_TAG\"}" > build.json
|
||||||
|
|
||||||
# 时区
|
# 时区
|
||||||
ENV TZ="Asia/Shanghai"
|
ENV TZ="Asia/Shanghai"
|
||||||
|
|
||||||
#构建版本
|
|
||||||
ARG BUILD_SHA
|
|
||||||
ARG BUILD_TAG
|
|
||||||
ENV BUILD_SHA=$BUILD_SHA
|
|
||||||
ENV BUILD_TAG=$BUILD_TAG
|
|
||||||
|
|
||||||
# 端口
|
# 端口
|
||||||
EXPOSE 5005
|
EXPOSE 5005
|
||||||
|
|
||||||
|
|||||||
124
README.md
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
对于一些持续更新的资源,隔段时间去转存十分麻烦。
|
对于一些持续更新的资源,隔段时间去转存十分麻烦。
|
||||||
|
|
||||||
定期执行本脚本自动转存、文件名整理,配合 Alist, rclone, Emby 可达到自动追更的效果。🥳
|
定期执行本脚本自动转存、文件名整理,配合 [SmartStrm](https://github.com/Cp0204/SmartStrm) / [OpenList](https://github.com/OpenListTeam/OpenList) , Emby 可达到自动追更的效果。🥳
|
||||||
|
|
||||||
|
|
||||||
[![wiki][wiki-image]][wiki-url] [![github releases][gitHub-releases-image]][github-url] [![docker pulls][docker-pulls-image]][docker-url] [![docker image size][docker-image-size-image]][docker-url]
|
[![wiki][wiki-image]][wiki-url] [![github releases][gitHub-releases-image]][github-url] [![docker pulls][docker-pulls-image]][docker-url] [![docker image size][docker-image-size-image]][docker-url]
|
||||||
@ -29,18 +29,19 @@
|
|||||||
> ⛔️⛔️⛔️ 注意!资源不会每时每刻更新,**严禁设定过高的定时运行频率!** 以免账号风控和给夸克服务器造成不必要的压力。雪山崩塌,每一片雪花都有责任!
|
> ⛔️⛔️⛔️ 注意!资源不会每时每刻更新,**严禁设定过高的定时运行频率!** 以免账号风控和给夸克服务器造成不必要的压力。雪山崩塌,每一片雪花都有责任!
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 因不想当客服处理各种使用咨询,即日起 Issues 关闭,如果你发现了 bug 、有好的想法或功能建议,欢迎通过 PR 和我对话,谢谢!
|
> 开发者≠客服,开源免费≠帮你解决使用问题;本项目 Wiki 已经相对完善,遇到问题请先翻阅 Issues 和 Wiki ,请勿盲目发问。
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
- 部署方式
|
- 部署方式
|
||||||
- [x] 兼容青龙
|
- [x] 可能~~兼容青龙~~
|
||||||
- [x] 支持 Docker 独立部署,WebUI 配置
|
- [x] Docker 部署,WebUI 配置
|
||||||
|
|
||||||
- 分享链接
|
- 分享链接
|
||||||
- [x] 支持分享链接的子目录
|
- [x] 支持分享链接的子目录
|
||||||
- [x] 记录失效分享并跳过任务
|
- [x] 记录失效分享并跳过任务
|
||||||
- [x] 支持需提取码的分享链接 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7%E9%9B%86%E9%94%A6#%E6%94%AF%E6%8C%81%E9%9C%80%E6%8F%90%E5%8F%96%E7%A0%81%E7%9A%84%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5)</sup>
|
- [x] 支持需提取码的分享链接 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#支持需提取码的分享链接)</sup>
|
||||||
|
- [x] 智能搜索资源并自动填充 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源)</sup>
|
||||||
|
|
||||||
- 文件管理
|
- 文件管理
|
||||||
- [x] 目标目录不存在时自动新建
|
- [x] 目标目录不存在时自动新建
|
||||||
@ -57,26 +58,26 @@
|
|||||||
- 媒体库整合
|
- 媒体库整合
|
||||||
- [x] 根据任务名搜索 Emby 媒体库
|
- [x] 根据任务名搜索 Emby 媒体库
|
||||||
- [x] 追更或整理后自动刷新 Emby 媒体库
|
- [x] 追更或整理后自动刷新 Emby 媒体库
|
||||||
- [x] **媒体库模块化,用户可很方便地[开发自己的媒体库hook模块](./media_servers)**
|
- [x] 插件模块化,允许自行开发和挂载[插件](./plugins)
|
||||||
|
|
||||||
- 其它
|
- 其它
|
||||||
- [x] 每日签到领空间 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7%E9%9B%86%E9%94%A6#%E6%AF%8F%E6%97%A5%E7%AD%BE%E5%88%B0%E9%A2%86%E7%A9%BA%E9%97%B4)</sup>
|
- [x] 每日签到领空间 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间)</sup>
|
||||||
- [x] 支持多个通知推送渠道 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/%E9%80%9A%E7%9F%A5%E6%8E%A8%E9%80%81%E6%9C%8D%E5%8A%A1%E9%85%8D%E7%BD%AE)</sup>
|
- [x] 支持多个通知推送渠道 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/通知推送服务配置)</sup>
|
||||||
- [x] 支持多账号(多账号签到,仅首账号转存)
|
- [x] 支持多账号(多账号签到,仅首账号转存)
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
### Docker 部署
|
### Docker 部署
|
||||||
|
|
||||||
Docker 部署提供 WebUI 管理配置,图形化配置已能满足绝大多数需求。部署命令:
|
Docker 部署提供 WebUI 进行管理配置,部署命令:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name quark-auto-save \
|
--name quark-auto-save \
|
||||||
-p 5005:5005 \
|
-p 5005:5005 \ # 映射端口,:前的可以改,即部署后访问的端口,:后的不可改
|
||||||
-e WEBUI_USERNAME=admin \
|
-e WEBUI_USERNAME=admin \
|
||||||
-e WEBUI_PASSWORD=admin123 \
|
-e WEBUI_PASSWORD=admin123 \
|
||||||
-v ./quark-auto-save/config:/app/config \
|
-v ./quark-auto-save/config:/app/config \ # 必须,配置持久化
|
||||||
-v ./quark-auto-save/media:/media \ # 可选,模块alist_strm_gen生成strm使用
|
-v ./quark-auto-save/media:/media \ # 可选,模块alist_strm_gen生成strm使用
|
||||||
--network bridge \
|
--network bridge \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
@ -106,10 +107,13 @@ services:
|
|||||||
|
|
||||||
管理地址:http://yourhost:5005
|
管理地址:http://yourhost:5005
|
||||||
|
|
||||||
| 环境变量 | 默认 | 备注 |
|
| 环境变量 | 默认 | 备注 |
|
||||||
| ---------------- | ---------- | -------- |
|
| ---------------- | ---------- | ---------------------------------------- |
|
||||||
| `WEBUI_USERNAME` | `admin` | 管理账号 |
|
| `WEBUI_USERNAME` | `admin` | 管理账号 |
|
||||||
| `WEBUI_PASSWORD` | `admin123` | 管理密码 |
|
| `WEBUI_PASSWORD` | `admin123` | 管理密码 |
|
||||||
|
| `PORT` | `5005` | 管理后台端口 |
|
||||||
|
| `PLUGIN_FLAGS` | | 插件标志,如 `-emby,-aria2` 禁用某些插件 |
|
||||||
|
| `TASK_TIMEOUT` | `1800` | 任务执行超时时间(秒),超时则任务结束 |
|
||||||
|
|
||||||
#### 一键更新
|
#### 一键更新
|
||||||
|
|
||||||
@ -126,51 +130,89 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### 青龙部署
|
|
||||||
|
|
||||||
程序也支持以青龙定时任务的方式运行,但该方式无法使用 WebUI 管理任务,需手动修改配置文件。
|
|
||||||
|
|
||||||
青龙部署说明已转移到 Wiki :[青龙部署教程](https://github.com/Cp0204/quark-auto-save/wiki/部署教程#青龙部署)
|
|
||||||
|
|
||||||
## 使用说明
|
## 使用说明
|
||||||
|
|
||||||
### 正则整理示例
|
### 正则处理示例
|
||||||
|
|
||||||
| pattern | replace | 效果 |
|
| pattern | replace | 效果 |
|
||||||
| -------------------------------------- | ------------ | ---------------------------------------------------------------------- |
|
| -------------------------------------- | ----------------------- | ---------------------------------------------------------------------- |
|
||||||
| `.*` | | 无脑转存所有文件,不整理 |
|
| `.*` | | 无脑转存所有文件,不整理 |
|
||||||
| `\.mp4$` | | 转存所有 `.mp4` 后缀的文件 |
|
| `\.mp4$` | | 转存所有 `.mp4` 后缀的文件 |
|
||||||
| `^【电影TT】花好月圆(\d+)\.(mp4\|mkv)` | `\1.\2` | 【电影TT】花好月圆01.mp4 → 01.mp4<br>【电影TT】花好月圆02.mkv → 02.mkv |
|
| `^【电影TT】花好月圆(\d+)\.(mp4\|mkv)` | `\1.\2` | 【电影TT】花好月圆01.mp4 → 01.mp4<br>【电影TT】花好月圆02.mkv → 02.mkv |
|
||||||
| `^(\d+)\.mp4` | `S02E\1.mp4` | 01.mp4 → S02E01.mp4<br>02.mp4 → S02E02.mp4 |
|
| `^(\d+)\.mp4` | `S02E\1.mp4` | 01.mp4 → S02E01.mp4<br>02.mp4 → S02E02.mp4 |
|
||||||
| `$TV` | | [魔法匹配](#魔法匹配)剧集文件 |
|
| `$TV` | | [魔法匹配](#魔法匹配)剧集文件 |
|
||||||
| `^(\d+)\.mp4` | `$TASKNAME.S02E\1.mp4` | 01.mp4 → 任务名.S02E01.mp4 |
|
| `^(\d+)\.mp4` | `{TASKNAME}.S02E\1.mp4` | 01.mp4 → 任务名.S02E01.mp4 |
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
>
|
|
||||||
> **魔法匹配**:当任务 `pattern` 值为 `$开头` 且 `replace` 留空时,实际将调用程序预设的正则表达式。
|
|
||||||
>
|
|
||||||
> 如 `$TV` 可适配和自动整理市面上90%分享剧集的文件名格式,具体实现见代码,欢迎贡献规则。
|
|
||||||
|
|
||||||
更多正则使用说明:[正则处理教程](https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程)
|
更多正则使用说明:[正则处理教程](https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程)
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> **魔法匹配和魔法变量**:在正则处理中,我们定义了一些“魔法匹配”模式,如果 表达式 的值以 $ 开头且 替换式 留空,程序将自动使用预设的正则表达式进行匹配和替换。
|
||||||
|
>
|
||||||
|
> 自 v0.6.0 开始,支持更多以 {} 包裹的我称之为“魔法变量”,可以更灵活地进行重命名。
|
||||||
|
>
|
||||||
|
> 更多说明请看[魔法匹配和魔法变量](https://github.com/Cp0204/quark-auto-save/wiki/魔法匹配和魔法变量)
|
||||||
|
|
||||||
### 刷新媒体库
|
### 刷新媒体库
|
||||||
|
|
||||||
在有新转存时,可触发完成相应功能,如自动刷新媒体库、生成 .strm 文件等。配置指南:[媒体库模块配置](https://github.com/Cp0204/quark-auto-save/wiki/媒体库模块配置)
|
在有新转存时,可触发完成相应功能,如自动刷新媒体库、生成 .strm 文件等。配置指南:[插件配置](https://github.com/Cp0204/quark-auto-save/wiki/插件配置)
|
||||||
|
|
||||||
媒体库模块以模块化方式的集成,如果你有兴趣请参考[媒体库模块开发指南](https://github.com/Cp0204/quark-auto-save/tree/main/media_servers)。
|
媒体库模块以插件的方式的集成,如果你有兴趣请参考[插件开发指南](https://github.com/Cp0204/quark-auto-save/tree/main/plugins)。
|
||||||
|
|
||||||
### 更多使用技巧
|
### 更多使用技巧
|
||||||
|
|
||||||
请参考 Wiki :[使用技巧集锦](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦)
|
请参考 Wiki :[使用技巧集锦](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦)
|
||||||
|
|
||||||
|
## 生态项目
|
||||||
|
|
||||||
|
以下展示 QAS 生态项目,包括官方项目和第三方项目。
|
||||||
|
|
||||||
|
### 官方项目
|
||||||
|
|
||||||
|
* [QAS一键推送助手](https://greasyfork.org/zh-CN/scripts/533201-qas一键推送助手)
|
||||||
|
|
||||||
|
油猴脚本,在夸克网盘分享页面添加推送到 QAS 的按钮
|
||||||
|
|
||||||
|
* [SmartStrm](https://github.com/Cp0204/SmartStrm)
|
||||||
|
|
||||||
|
STRM 文件生成工具,用于转存后处理,媒体免下载入库播放。
|
||||||
|
|
||||||
|
### 第三方开源项目
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> 以下第三方开源项目均由社区开发并保持开源,与 QAS 作者无直接关联。在部署到生产环境前,请自行评估相关风险。
|
||||||
|
>
|
||||||
|
> 如果您有新的项目没有在此列出,可以通过 Issues 提交。
|
||||||
|
|
||||||
|
* [nonebot-plugin-quark-autosave](https://github.com/fllesser/nonebot-plugin-quark-autosave)
|
||||||
|
|
||||||
|
QAS Telegram 机器人,快速管理自动转存任务
|
||||||
|
|
||||||
|
* [Astrbot_plugin_quarksave](https://github.com/lm379/astrbot_plugin_quarksave)
|
||||||
|
|
||||||
|
AstrBot 插件,调用 quark_auto_save 实现自动转存资源到夸克网盘
|
||||||
|
|
||||||
|
* [Telegram 媒体资源管理机器人](https://github.com/2beetle/tgbot)
|
||||||
|
|
||||||
|
一个功能丰富的 Telegram 机器人,专注于媒体资源管理、Emby 集成、自动下载和夸克网盘资源管理。
|
||||||
|
|
||||||
## 打赏
|
## 打赏
|
||||||
|
|
||||||
如果这个项目让你受益,你可以打赏我1块钱,让我知道开源有价值。谢谢!
|
如果这个项目让你受益,你可以无偿赠与我1块钱,让我知道开源有价值。谢谢!
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 声明
|
## 声明
|
||||||
|
|
||||||
本程序为个人兴趣开发,开源仅供学习与交流使用。
|
本项目为个人兴趣开发,旨在通过程序自动化提高网盘使用效率。
|
||||||
|
|
||||||
程序没有任何破解行为,只是对于夸克已有的API进行封装,所有数据来自于夸克官方API,本人不对网盘内容负责、不对夸克官方API未来可能的改动导致的后果负责。
|
程序没有任何破解行为,只是对于夸克已有的API进行封装,所有数据来自于夸克官方API;本人不对网盘内容负责、不对夸克官方API未来可能的变动导致的影响负责,请自行斟酌使用。
|
||||||
|
|
||||||
|
开源仅供学习与交流使用,未盈利也未授权商业使用,严禁用于非法用途。
|
||||||
|
|
||||||
|
## Sponsor
|
||||||
|
|
||||||
|
CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne.
|
||||||
|
|
||||||
|
<a href="https://edgeone.ai/?from=github" target="_blank"><img title="Best Asian CDN, Edge, and Secure Solutions - Tencent EdgeOne" src="https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png" width="300"></a>
|
||||||
463
app/run.py
@ -1,6 +1,7 @@
|
|||||||
# !/usr/bin/env python3
|
# !/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from flask import (
|
from flask import (
|
||||||
|
json,
|
||||||
Flask,
|
Flask,
|
||||||
url_for,
|
url_for,
|
||||||
session,
|
session,
|
||||||
@ -14,21 +15,48 @@ from flask import (
|
|||||||
)
|
)
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from sdk.cloudsaver import CloudSaver
|
||||||
|
from sdk.pansou import PanSou
|
||||||
|
from datetime import timedelta
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import requests
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import json
|
import traceback
|
||||||
|
import base64
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
from quark_auto_save import Quark
|
from quark_auto_save import Quark, Config, MagicRename
|
||||||
|
|
||||||
|
print(
|
||||||
|
r"""
|
||||||
|
____ ___ _____
|
||||||
|
/ __ \ / | / ___/
|
||||||
|
/ / / / / /| | \__ \
|
||||||
|
/ /_/ / / ___ |___/ /
|
||||||
|
\___\_\/_/ |_/____/
|
||||||
|
|
||||||
|
-- Quark-Auto-Save --
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
def get_app_ver():
|
def get_app_ver():
|
||||||
BUILD_SHA = os.environ.get("BUILD_SHA", "")
|
"""获取应用版本"""
|
||||||
BUILD_TAG = os.environ.get("BUILD_TAG", "")
|
try:
|
||||||
|
with open("build.json", "r") as f:
|
||||||
|
build_info = json.loads(f.read())
|
||||||
|
BUILD_SHA = build_info["BUILD_SHA"]
|
||||||
|
BUILD_TAG = build_info["BUILD_TAG"]
|
||||||
|
except Exception as e:
|
||||||
|
BUILD_SHA = os.getenv("BUILD_SHA", "")
|
||||||
|
BUILD_TAG = os.getenv("BUILD_TAG", "")
|
||||||
if BUILD_TAG[:1] == "v":
|
if BUILD_TAG[:1] == "v":
|
||||||
return BUILD_TAG
|
return BUILD_TAG
|
||||||
elif BUILD_SHA:
|
elif BUILD_SHA:
|
||||||
@ -41,12 +69,20 @@ def get_app_ver():
|
|||||||
PYTHON_PATH = "python3" if os.path.exists("/usr/bin/python3") else "python"
|
PYTHON_PATH = "python3" if os.path.exists("/usr/bin/python3") else "python"
|
||||||
SCRIPT_PATH = os.environ.get("SCRIPT_PATH", "./quark_auto_save.py")
|
SCRIPT_PATH = os.environ.get("SCRIPT_PATH", "./quark_auto_save.py")
|
||||||
CONFIG_PATH = os.environ.get("CONFIG_PATH", "./config/quark_config.json")
|
CONFIG_PATH = os.environ.get("CONFIG_PATH", "./config/quark_config.json")
|
||||||
DEBUG = os.environ.get("DEBUG", False)
|
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "")
|
||||||
|
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
|
||||||
|
HOST = os.environ.get("HOST", "0.0.0.0")
|
||||||
|
PORT = os.environ.get("PORT", 5005)
|
||||||
|
TASK_TIMEOUT = int(os.environ.get("TASK_TIMEOUT", 1800))
|
||||||
|
|
||||||
|
config_data = {}
|
||||||
|
task_plugins_config_default = {}
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config["APP_VERSION"] = get_app_ver()
|
app.config["APP_VERSION"] = get_app_ver()
|
||||||
app.secret_key = "ca943f6db6dd34823d36ab08d8d6f65d"
|
app.secret_key = "ca943f6db6dd34823d36ab08d8d6f65d"
|
||||||
app.config["SESSION_COOKIE_NAME"] = "QUARK_AUTO_SAVE_SESSION"
|
app.config["SESSION_COOKIE_NAME"] = "QUARK_AUTO_SAVE_SESSION"
|
||||||
|
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=31)
|
||||||
app.json.ensure_ascii = False
|
app.json.ensure_ascii = False
|
||||||
app.json.sort_keys = False
|
app.json.sort_keys = False
|
||||||
app.jinja_env.variable_start_string = "[["
|
app.jinja_env.variable_start_string = "[["
|
||||||
@ -61,6 +97,8 @@ logging.basicConfig(
|
|||||||
# 过滤werkzeug日志输出
|
# 过滤werkzeug日志输出
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||||
|
logging.getLogger("apscheduler").setLevel(logging.ERROR)
|
||||||
|
sys.modules["flask.cli"].show_server_banner = lambda *x: None
|
||||||
|
|
||||||
|
|
||||||
def gen_md5(string):
|
def gen_md5(string):
|
||||||
@ -69,24 +107,15 @@ def gen_md5(string):
|
|||||||
return md5.hexdigest()
|
return md5.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
# 读取 JSON 文件内容
|
def get_login_token():
|
||||||
def read_json():
|
username = config_data["webui"]["username"]
|
||||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
password = config_data["webui"]["password"]
|
||||||
data = json.load(f)
|
return gen_md5(f"token{username}{password}+-*/")[8:24]
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# 将数据写入 JSON 文件
|
|
||||||
def write_json(data):
|
|
||||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f, indent=4, ensure_ascii=False, sort_keys=False)
|
|
||||||
|
|
||||||
|
|
||||||
def is_login():
|
def is_login():
|
||||||
data = read_json()
|
login_token = get_login_token()
|
||||||
username = data["webui"]["username"]
|
if session.get("token") == login_token or request.args.get("token") == login_token:
|
||||||
password = data["webui"]["password"]
|
|
||||||
if session.get("login") == gen_md5(username + password):
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@ -106,27 +135,29 @@ def favicon():
|
|||||||
@app.route("/login", methods=["GET", "POST"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
data = read_json()
|
username = config_data["webui"]["username"]
|
||||||
username = data["webui"]["username"]
|
password = config_data["webui"]["password"]
|
||||||
password = data["webui"]["password"]
|
|
||||||
# 验证用户名和密码
|
# 验证用户名和密码
|
||||||
if (username == request.form.get("username")) and (
|
if (username == request.form.get("username")) and (
|
||||||
password == request.form.get("password")
|
password == request.form.get("password")
|
||||||
):
|
):
|
||||||
logging.info(f">>> 用户 {username} 登录成功")
|
logging.info(f">>> 用户 {username} 登录成功")
|
||||||
session["login"] = gen_md5(username + password)
|
session.permanent = True
|
||||||
|
session["token"] = get_login_token()
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
else:
|
else:
|
||||||
logging.info(f">>> 用户 {username} 登录失败")
|
logging.info(f">>> 用户 {username} 登录失败")
|
||||||
return render_template("login.html", message="登录失败")
|
return render_template("login.html", message="登录失败")
|
||||||
|
|
||||||
|
if is_login():
|
||||||
|
return redirect(url_for("index"))
|
||||||
return render_template("login.html", error=None)
|
return render_template("login.html", error=None)
|
||||||
|
|
||||||
|
|
||||||
# 退出登录
|
# 退出登录
|
||||||
@app.route("/logout")
|
@app.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
session.pop("login", None)
|
session.pop("token", None)
|
||||||
return redirect(url_for("login"))
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
|
||||||
@ -135,53 +166,68 @@ def logout():
|
|||||||
def index():
|
def index():
|
||||||
if not is_login():
|
if not is_login():
|
||||||
return redirect(url_for("login"))
|
return redirect(url_for("login"))
|
||||||
return render_template("index.html", version=app.config["APP_VERSION"])
|
return render_template(
|
||||||
|
"index.html", version=app.config["APP_VERSION"], plugin_flags=PLUGIN_FLAGS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 获取配置数据
|
# 获取配置数据
|
||||||
@app.route("/data")
|
@app.route("/data")
|
||||||
def get_data():
|
def get_data():
|
||||||
if not is_login():
|
if not is_login():
|
||||||
return redirect(url_for("login"))
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
data = read_json()
|
data = Config.read_json(CONFIG_PATH)
|
||||||
del data["webui"]
|
del data["webui"]
|
||||||
return jsonify(data)
|
data["api_token"] = get_login_token()
|
||||||
|
data["task_plugins_config_default"] = task_plugins_config_default
|
||||||
|
return jsonify({"success": True, "data": data})
|
||||||
|
|
||||||
|
|
||||||
# 更新数据
|
# 更新数据
|
||||||
@app.route("/update", methods=["POST"])
|
@app.route("/update", methods=["POST"])
|
||||||
def update():
|
def update():
|
||||||
|
global config_data
|
||||||
if not is_login():
|
if not is_login():
|
||||||
return "未登录"
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
data = read_json()
|
dont_save_keys = ["task_plugins_config_default", "api_token"]
|
||||||
webui = data["webui"]
|
for key, value in request.json.items():
|
||||||
data = request.json
|
if key not in dont_save_keys:
|
||||||
data["webui"] = webui
|
config_data.update({key: value})
|
||||||
write_json(data)
|
Config.write_json(CONFIG_PATH, config_data)
|
||||||
# 重新加载任务
|
# 重新加载任务
|
||||||
if reload_tasks():
|
if reload_tasks():
|
||||||
logging.info(f">>> 配置更新成功")
|
logging.info(f">>> 配置更新成功")
|
||||||
return "配置更新成功"
|
return jsonify({"success": True, "message": "配置更新成功"})
|
||||||
else:
|
else:
|
||||||
logging.info(f">>> 配置更新失败")
|
logging.info(f">>> 配置更新失败")
|
||||||
return "配置更新失败"
|
return jsonify({"success": False, "message": "配置更新失败"})
|
||||||
|
|
||||||
|
|
||||||
# 处理运行脚本请求
|
# 处理运行脚本请求
|
||||||
@app.route("/run_script_now", methods=["GET"])
|
@app.route("/run_script_now", methods=["POST"])
|
||||||
def run_script_now():
|
def run_script_now():
|
||||||
if not is_login():
|
if not is_login():
|
||||||
return "未登录"
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
task_index = request.args.get("task_index", "")
|
tasklist = request.json.get("tasklist", [])
|
||||||
command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH, task_index]
|
command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH]
|
||||||
logging.info(
|
logging.info(
|
||||||
f">>> 手动运行任务{int(task_index)+1 if task_index.isdigit() else 'all'}"
|
f">>> 手动运行任务 [{tasklist[0].get('taskname') if len(tasklist)>0 else 'ALL'}] 开始执行..."
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_output():
|
def generate_output():
|
||||||
# 设置环境变量
|
# 设置环境变量
|
||||||
process_env = os.environ.copy()
|
process_env = os.environ.copy()
|
||||||
process_env["PYTHONIOENCODING"] = "utf-8"
|
process_env["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
if request.json.get("quark_test"):
|
||||||
|
process_env["QUARK_TEST"] = "true"
|
||||||
|
process_env["COOKIE"] = json.dumps(
|
||||||
|
request.json.get("cookie", []), ensure_ascii=False
|
||||||
|
)
|
||||||
|
process_env["PUSH_CONFIG"] = json.dumps(
|
||||||
|
request.json.get("push_config", {}), ensure_ascii=False
|
||||||
|
)
|
||||||
|
if tasklist:
|
||||||
|
process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False)
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
command,
|
command,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
@ -207,52 +253,276 @@ def run_script_now():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/get_share_detail")
|
@app.route("/task_suggestions")
|
||||||
def get_share_files():
|
def get_task_suggestions():
|
||||||
if not is_login():
|
if not is_login():
|
||||||
return jsonify({"error": "未登录"})
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
shareurl = request.args.get("shareurl", "")
|
query = request.args.get("q", "").lower()
|
||||||
account = Quark("", 0)
|
deep = request.args.get("d", "").lower()
|
||||||
pwd_id, passcode, pdir_fid = account.get_id_from_url(shareurl)
|
net_data = config_data.get("source", {}).get("net", {})
|
||||||
is_sharing, stoken = account.get_stoken(pwd_id, passcode)
|
cs_data = config_data.get("source", {}).get("cloudsaver", {})
|
||||||
if not is_sharing:
|
ps_data = config_data.get("source", {}).get("pansou", {})
|
||||||
return jsonify({"error": stoken})
|
|
||||||
share_detail = account.get_detail(pwd_id, stoken, pdir_fid, 1)
|
def net_search():
|
||||||
return jsonify(share_detail)
|
if str(net_data.get("enable", "true")).lower() != "false":
|
||||||
|
base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
|
||||||
|
url = f"{base_url}/task_suggestions?q={query}&d={deep}"
|
||||||
|
response = requests.get(url)
|
||||||
|
return response.json()
|
||||||
|
return []
|
||||||
|
|
||||||
|
def cs_search():
|
||||||
|
if (
|
||||||
|
cs_data.get("server")
|
||||||
|
and cs_data.get("username")
|
||||||
|
and cs_data.get("password")
|
||||||
|
):
|
||||||
|
cs = CloudSaver(cs_data.get("server"))
|
||||||
|
cs.set_auth(
|
||||||
|
cs_data.get("username", ""),
|
||||||
|
cs_data.get("password", ""),
|
||||||
|
cs_data.get("token", ""),
|
||||||
|
)
|
||||||
|
search = cs.auto_login_search(query)
|
||||||
|
if search.get("success"):
|
||||||
|
if search.get("new_token"):
|
||||||
|
cs_data["token"] = search.get("new_token")
|
||||||
|
Config.write_json(CONFIG_PATH, config_data)
|
||||||
|
search_results = cs.clean_search_results(search.get("data"))
|
||||||
|
return search_results
|
||||||
|
return []
|
||||||
|
|
||||||
|
def ps_search():
|
||||||
|
if ps_data.get("server"):
|
||||||
|
ps = PanSou(ps_data.get("server"))
|
||||||
|
return ps.search(query, deep == "1")
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
search_results = []
|
||||||
|
with ThreadPoolExecutor(max_workers=3) as executor:
|
||||||
|
features = []
|
||||||
|
features.append(executor.submit(net_search))
|
||||||
|
features.append(executor.submit(cs_search))
|
||||||
|
features.append(executor.submit(ps_search))
|
||||||
|
for future in as_completed(features):
|
||||||
|
result = future.result()
|
||||||
|
search_results.extend(result)
|
||||||
|
|
||||||
|
# 按时间排序并去重
|
||||||
|
results = []
|
||||||
|
link_array = []
|
||||||
|
search_results.sort(key=lambda x: x.get("datetime", ""), reverse=True)
|
||||||
|
for item in search_results:
|
||||||
|
url = item.get("shareurl", "")
|
||||||
|
if url != "" and url not in link_array:
|
||||||
|
link_array.append(url)
|
||||||
|
results.append(item)
|
||||||
|
|
||||||
|
return jsonify({"success": True, "data": results})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": True, "message": f"error: {str(e)}"})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/get_savepath")
|
@app.route("/get_share_detail", methods=["POST"])
|
||||||
def get_savepath():
|
def get_share_detail():
|
||||||
if not is_login():
|
if not is_login():
|
||||||
return jsonify({"error": "未登录"})
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
data = read_json()
|
shareurl = request.json.get("shareurl", "")
|
||||||
account = Quark(data["cookie"][0], 0)
|
stoken = request.json.get("stoken", "")
|
||||||
|
account = Quark()
|
||||||
|
pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl)
|
||||||
|
if not stoken:
|
||||||
|
get_stoken = account.get_stoken(pwd_id, passcode)
|
||||||
|
if get_stoken.get("status") == 200:
|
||||||
|
stoken = get_stoken["data"]["stoken"]
|
||||||
|
else:
|
||||||
|
return jsonify(
|
||||||
|
{"success": False, "data": {"error": get_stoken.get("message")}}
|
||||||
|
)
|
||||||
|
share_detail = account.get_detail(
|
||||||
|
pwd_id, stoken, pdir_fid, _fetch_share=1, fetch_share_full_path=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if share_detail.get("code") != 0:
|
||||||
|
return jsonify(
|
||||||
|
{"success": False, "data": {"error": share_detail.get("message")}}
|
||||||
|
)
|
||||||
|
|
||||||
|
data = share_detail["data"]
|
||||||
|
data["paths"] = [
|
||||||
|
{"fid": i["fid"], "name": i["file_name"]}
|
||||||
|
for i in share_detail["data"].get("full_path", [])
|
||||||
|
] or paths
|
||||||
|
data["stoken"] = stoken
|
||||||
|
|
||||||
|
# 正则处理预览
|
||||||
|
def preview_regex(data):
|
||||||
|
task = request.json.get("task", {})
|
||||||
|
magic_regex = request.json.get("magic_regex", {})
|
||||||
|
mr = MagicRename(magic_regex)
|
||||||
|
mr.set_taskname(task.get("taskname", ""))
|
||||||
|
account = Quark(config_data["cookie"][0])
|
||||||
|
get_fids = account.get_fids([task.get("savepath", "")])
|
||||||
|
if get_fids:
|
||||||
|
dir_file_list = account.ls_dir(get_fids[0]["fid"])["data"]["list"]
|
||||||
|
dir_filename_list = [dir_file["file_name"] for dir_file in dir_file_list]
|
||||||
|
else:
|
||||||
|
dir_file_list = []
|
||||||
|
dir_filename_list = []
|
||||||
|
|
||||||
|
pattern, replace = mr.magic_regex_conv(
|
||||||
|
task.get("pattern", ""), task.get("replace", "")
|
||||||
|
)
|
||||||
|
for share_file in data["list"]:
|
||||||
|
search_pattern = (
|
||||||
|
task["update_subdir"]
|
||||||
|
if share_file["dir"] and task.get("update_subdir")
|
||||||
|
else pattern
|
||||||
|
)
|
||||||
|
if re.search(search_pattern, share_file["file_name"]):
|
||||||
|
# 文件名重命名,目录不重命名
|
||||||
|
file_name_re = (
|
||||||
|
share_file["file_name"]
|
||||||
|
if share_file["dir"]
|
||||||
|
else mr.sub(pattern, replace, share_file["file_name"])
|
||||||
|
)
|
||||||
|
if file_name_saved := mr.is_exists(
|
||||||
|
file_name_re,
|
||||||
|
dir_filename_list,
|
||||||
|
(task.get("ignore_extension") and not share_file["dir"]),
|
||||||
|
):
|
||||||
|
share_file["file_name_saved"] = file_name_saved
|
||||||
|
else:
|
||||||
|
share_file["file_name_re"] = file_name_re
|
||||||
|
|
||||||
|
# 文件列表排序
|
||||||
|
if re.search(r"\{I+\}", replace):
|
||||||
|
mr.set_dir_file_list(dir_file_list, replace)
|
||||||
|
mr.sort_file_list(data["list"])
|
||||||
|
|
||||||
|
if request.json.get("task"):
|
||||||
|
preview_regex(data)
|
||||||
|
|
||||||
|
return jsonify({"success": True, "data": data})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/get_savepath_detail")
|
||||||
|
def get_savepath_detail():
|
||||||
|
if not is_login():
|
||||||
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
|
account = Quark(config_data["cookie"][0])
|
||||||
|
paths = []
|
||||||
if path := request.args.get("path"):
|
if path := request.args.get("path"):
|
||||||
|
path = re.sub(r"/+", "/", path)
|
||||||
if path == "/":
|
if path == "/":
|
||||||
fid = 0
|
fid = 0
|
||||||
elif get_fids := account.get_fids([path]):
|
|
||||||
fid = get_fids[0]["fid"]
|
|
||||||
else:
|
else:
|
||||||
return jsonify([])
|
dir_names = path.split("/")
|
||||||
|
if dir_names[0] == "":
|
||||||
|
dir_names.pop(0)
|
||||||
|
path_fids = []
|
||||||
|
current_path = ""
|
||||||
|
for dir_name in dir_names:
|
||||||
|
current_path += "/" + dir_name
|
||||||
|
path_fids.append(current_path)
|
||||||
|
if get_fids := account.get_fids(path_fids):
|
||||||
|
fid = get_fids[-1]["fid"]
|
||||||
|
paths = [
|
||||||
|
{"fid": get_fid["fid"], "name": dir_name}
|
||||||
|
for get_fid, dir_name in zip(get_fids, dir_names)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "data": {"error": "获取fid失败"}})
|
||||||
else:
|
else:
|
||||||
fid = request.args.get("fid", 0)
|
fid = request.args.get("fid", "0")
|
||||||
file_list = account.ls_dir(fid)
|
file_list = {
|
||||||
return jsonify(file_list)
|
"list": account.ls_dir(fid)["data"]["list"],
|
||||||
|
"paths": paths,
|
||||||
|
}
|
||||||
|
return jsonify({"success": True, "data": file_list})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/delete_file", methods=["POST"])
|
||||||
|
def delete_file():
|
||||||
|
if not is_login():
|
||||||
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
|
account = Quark(config_data["cookie"][0])
|
||||||
|
if fid := request.json.get("fid"):
|
||||||
|
response = account.delete([fid])
|
||||||
|
else:
|
||||||
|
response = {"success": False, "message": "缺失必要字段: fid"}
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
|
# 添加任务接口
|
||||||
|
@app.route("/api/add_task", methods=["POST"])
|
||||||
|
def add_task():
|
||||||
|
global config_data
|
||||||
|
# 验证token
|
||||||
|
if not is_login():
|
||||||
|
return jsonify({"success": False, "code": 1, "message": "未登录"}), 401
|
||||||
|
# 必选字段
|
||||||
|
request_data = request.json
|
||||||
|
required_fields = ["taskname", "shareurl", "savepath"]
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in request_data or not request_data[field]:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{"success": False, "code": 2, "message": f"缺少必要字段: {field}"}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
if not request_data.get("addition"):
|
||||||
|
request_data["addition"] = task_plugins_config_default
|
||||||
|
# 添加任务
|
||||||
|
config_data["tasklist"].append(request_data)
|
||||||
|
Config.write_json(CONFIG_PATH, config_data)
|
||||||
|
logging.info(f">>> 通过API添加任务: {request_data['taskname']}")
|
||||||
|
return jsonify(
|
||||||
|
{"success": True, "code": 0, "message": "任务添加成功", "data": request_data}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 定时任务执行的函数
|
# 定时任务执行的函数
|
||||||
def run_python(args):
|
def run_python(args):
|
||||||
logging.info(f">>> 定时运行任务")
|
logging.info(f">>> 定时运行任务")
|
||||||
os.system(f"{PYTHON_PATH} {args}")
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
f"{PYTHON_PATH} {args}",
|
||||||
|
shell=True,
|
||||||
|
timeout=TASK_TIMEOUT,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
)
|
||||||
|
# 输出执行日志
|
||||||
|
if result.stdout:
|
||||||
|
for line in result.stdout.strip().split("\n"):
|
||||||
|
if line.strip():
|
||||||
|
logging.info(line)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
logging.info(f">>> 任务执行成功")
|
||||||
|
else:
|
||||||
|
logging.error(f">>> 任务执行失败,返回码: {result.returncode}")
|
||||||
|
if result.stderr:
|
||||||
|
logging.error(f"错误信息: {result.stderr[:500]}")
|
||||||
|
except subprocess.TimeoutExpired as e:
|
||||||
|
logging.error(f">>> 任务执行超时(>{TASK_TIMEOUT}s),强制终止")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f">>> 任务执行异常: {str(e)}")
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
# 确保函数能够正常返回
|
||||||
|
logging.debug(f">>> run_python 函数执行完成")
|
||||||
|
|
||||||
|
|
||||||
# 重新加载任务
|
# 重新加载任务
|
||||||
def reload_tasks():
|
def reload_tasks():
|
||||||
# 读取数据
|
# 读取定时规则
|
||||||
data = read_json()
|
if crontab := config_data.get("crontab"):
|
||||||
# 添加新任务
|
|
||||||
crontab = data.get("crontab")
|
|
||||||
if crontab:
|
|
||||||
if scheduler.state == 1:
|
if scheduler.state == 1:
|
||||||
scheduler.pause() # 暂停调度器
|
scheduler.pause() # 暂停调度器
|
||||||
trigger = CronTrigger.from_crontab(crontab)
|
trigger = CronTrigger.from_crontab(crontab)
|
||||||
@ -262,6 +532,10 @@ def reload_tasks():
|
|||||||
trigger=trigger,
|
trigger=trigger,
|
||||||
args=[f"{SCRIPT_PATH} {CONFIG_PATH}"],
|
args=[f"{SCRIPT_PATH} {CONFIG_PATH}"],
|
||||||
id=SCRIPT_PATH,
|
id=SCRIPT_PATH,
|
||||||
|
max_instances=1, # 最多允许1个实例运行
|
||||||
|
coalesce=True, # 合并错过的任务,避免堆积
|
||||||
|
misfire_grace_time=300, # 错过任务的宽限期(秒),超过则跳过
|
||||||
|
replace_existing=True, # 替换已存在的同ID任务
|
||||||
)
|
)
|
||||||
if scheduler.state == 0:
|
if scheduler.state == 0:
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
@ -279,32 +553,49 @@ def reload_tasks():
|
|||||||
|
|
||||||
|
|
||||||
def init():
|
def init():
|
||||||
logging.info(f">>> 初始化配置")
|
global config_data, task_plugins_config_default
|
||||||
|
logging.info(">>> 初始化配置")
|
||||||
# 检查配置文件是否存在
|
# 检查配置文件是否存在
|
||||||
if not os.path.exists(CONFIG_PATH):
|
if not os.path.exists(CONFIG_PATH):
|
||||||
if not os.path.exists(os.path.dirname(CONFIG_PATH)):
|
if not os.path.exists(os.path.dirname(CONFIG_PATH)):
|
||||||
os.makedirs(os.path.dirname(CONFIG_PATH))
|
os.makedirs(os.path.dirname(CONFIG_PATH))
|
||||||
with open("quark_config.json", "rb") as src, open(CONFIG_PATH, "wb") as dest:
|
with open("quark_config.json", "rb") as src, open(CONFIG_PATH, "wb") as dest:
|
||||||
dest.write(src.read())
|
dest.write(src.read())
|
||||||
data = read_json()
|
|
||||||
|
# 读取配置
|
||||||
|
config_data = Config.read_json(CONFIG_PATH)
|
||||||
|
Config.breaking_change_update(config_data)
|
||||||
|
if not config_data.get("magic_regex"):
|
||||||
|
config_data["magic_regex"] = MagicRename().magic_regex
|
||||||
|
|
||||||
# 默认管理账号
|
# 默认管理账号
|
||||||
if not data.get("webui"):
|
config_data["webui"] = {
|
||||||
data["webui"] = {
|
"username": os.environ.get("WEBUI_USERNAME")
|
||||||
"username": "admin",
|
or config_data.get("webui", {}).get("username", "admin"),
|
||||||
"password": "admin123",
|
"password": os.environ.get("WEBUI_PASSWORD")
|
||||||
}
|
or config_data.get("webui", {}).get("password", "admin123"),
|
||||||
elif os.environ.get("WEBUI_USERNAME") and os.environ.get("WEBUI_PASSWORD"):
|
}
|
||||||
data["webui"] = {
|
|
||||||
"username": os.environ.get("WEBUI_USERNAME"),
|
|
||||||
"password": os.environ.get("WEBUI_PASSWORD"),
|
|
||||||
}
|
|
||||||
# 默认定时规则
|
# 默认定时规则
|
||||||
if not data.get("crontab"):
|
if not config_data.get("crontab"):
|
||||||
data["crontab"] = "0 8,18,20 * * *"
|
config_data["crontab"] = "0 8,18,20 * * *"
|
||||||
write_json(data)
|
|
||||||
|
# 初始化插件配置
|
||||||
|
_, plugins_config_default, task_plugins_config_default = Config.load_plugins()
|
||||||
|
plugins_config_default.update(config_data.get("plugins", {}))
|
||||||
|
config_data["plugins"] = plugins_config_default
|
||||||
|
|
||||||
|
# 更新配置
|
||||||
|
Config.write_json(CONFIG_PATH, config_data)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
init()
|
init()
|
||||||
reload_tasks()
|
reload_tasks()
|
||||||
app.run(debug=DEBUG, host="0.0.0.0", port=5005)
|
logging.info(">>> 启动Web服务")
|
||||||
|
logging.info(f"运行在: http://{HOST}:{PORT}")
|
||||||
|
app.run(
|
||||||
|
debug=DEBUG,
|
||||||
|
host=HOST,
|
||||||
|
port=PORT,
|
||||||
|
)
|
||||||
|
|||||||
168
app/sdk/cloudsaver.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from sdk.common import iso_to_cst
|
||||||
|
|
||||||
|
|
||||||
|
class CloudSaver:
|
||||||
|
"""
|
||||||
|
CloudSaver 类,用于获取云盘资源
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, server):
|
||||||
|
self.server = server
|
||||||
|
self.username = None
|
||||||
|
self.password = None
|
||||||
|
self.token = None
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({"Content-Type": "application/json"})
|
||||||
|
|
||||||
|
def set_auth(self, username, password, token=""):
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.token = token
|
||||||
|
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
if not self.username or not self.password:
|
||||||
|
return {"success": False, "message": "CloudSaver未设置用户名或密码"}
|
||||||
|
try:
|
||||||
|
url = f"{self.server}/api/user/login"
|
||||||
|
data = {"username": self.username, "password": self.password}
|
||||||
|
response = self.session.post(url, json=data)
|
||||||
|
result = response.json()
|
||||||
|
if result.get("success"):
|
||||||
|
self.token = result.get("data", {}).get("token")
|
||||||
|
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
|
||||||
|
return {"success": True, "token": self.token}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"CloudSaver登录{result.get('message', '未知错误')}",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
def search(self, keyword, last_message_id=""):
|
||||||
|
"""
|
||||||
|
搜索资源
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyword (str): 搜索关键词
|
||||||
|
last_message_id (str): 上一条消息ID,用于分页
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: 搜索结果列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{self.server}/api/search"
|
||||||
|
params = {"keyword": keyword, "lastMessageId": last_message_id}
|
||||||
|
response = self.session.get(url, params=params)
|
||||||
|
result = response.json()
|
||||||
|
if result.get("success"):
|
||||||
|
data = result.get("data", [])
|
||||||
|
return {"success": True, "data": data}
|
||||||
|
else:
|
||||||
|
return {"success": False, "message": result.get("message", "未知错误")}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
def auto_login_search(self, keyword, last_message_id=""):
|
||||||
|
"""
|
||||||
|
自动登录并搜索资源
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyword (str): 搜索关键词
|
||||||
|
last_message_id (str): 上一条消息ID,用于分页
|
||||||
|
"""
|
||||||
|
result = self.search(keyword, last_message_id)
|
||||||
|
if result.get("success"):
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
result.get("message") == "无效的 token"
|
||||||
|
or result.get("message") == "未提供 token"
|
||||||
|
):
|
||||||
|
login_result = self.login()
|
||||||
|
if login_result.get("success"):
|
||||||
|
result = self.search(keyword, last_message_id)
|
||||||
|
result["new_token"] = login_result.get("token")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": login_result.get("message", "未知错误"),
|
||||||
|
}
|
||||||
|
return {"success": False, "message": result.get("message", "未知错误")}
|
||||||
|
|
||||||
|
def clean_search_results(self, search_results):
|
||||||
|
"""
|
||||||
|
清洗搜索结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_results (list): 搜索结果列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: 夸克网盘链接列表
|
||||||
|
"""
|
||||||
|
pattern_title = r"(名称|标题)[::]?(.*)"
|
||||||
|
pattern_content = r"(描述|简介)[::]?(.*)(链接|标签)"
|
||||||
|
clean_results = []
|
||||||
|
link_array = []
|
||||||
|
for channel in search_results:
|
||||||
|
for item in channel.get("list", []):
|
||||||
|
cloud_links = item.get("cloudLinks", [])
|
||||||
|
for link in cloud_links:
|
||||||
|
if link.get("cloudType") == "quark":
|
||||||
|
# 清洗标题
|
||||||
|
title = item.get("title", "")
|
||||||
|
if match := re.search(pattern_title, title, re.DOTALL):
|
||||||
|
title = match.group(2)
|
||||||
|
title = title.replace("&", "&").strip()
|
||||||
|
# 清洗内容
|
||||||
|
content = item.get("content", "")
|
||||||
|
if match := re.search(pattern_content, content, re.DOTALL):
|
||||||
|
content = match.group(2)
|
||||||
|
content = content.replace('<mark class="highlight">', "")
|
||||||
|
content = content.replace("</mark>", "")
|
||||||
|
content = content.strip()
|
||||||
|
# 统一发布时间格式
|
||||||
|
pubdate = item.get("pubDate", "")
|
||||||
|
if pubdate:
|
||||||
|
pubdate = iso_to_cst(pubdate)
|
||||||
|
# 链接去重
|
||||||
|
if link.get("link") not in link_array:
|
||||||
|
link_array.append(link.get("link"))
|
||||||
|
clean_results.append(
|
||||||
|
{
|
||||||
|
"shareurl": link.get("link"),
|
||||||
|
"taskname": title,
|
||||||
|
"content": content,
|
||||||
|
"datetime": pubdate,
|
||||||
|
"tags": item.get("tags", []),
|
||||||
|
"channel": item.get("channelId", ""),
|
||||||
|
"source": "CloudSaver"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return clean_results
|
||||||
|
|
||||||
|
|
||||||
|
# 测试示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 创建CloudSaver实例
|
||||||
|
server = ""
|
||||||
|
username = ""
|
||||||
|
password = ""
|
||||||
|
token = ""
|
||||||
|
cloud_saver = CloudSaver(server)
|
||||||
|
cloud_saver.set_auth(username, password, token)
|
||||||
|
# 搜索资源
|
||||||
|
results = cloud_saver.auto_login_search("黑镜")
|
||||||
|
# 提取夸克网盘链接
|
||||||
|
clean_results = cloud_saver.clean_search_results(results.get("data", []))
|
||||||
|
# 打印结果
|
||||||
|
for item in clean_results:
|
||||||
|
print(f"标题: {item['taskname']}")
|
||||||
|
print(f"描述: {item['content']}")
|
||||||
|
print(f"链接: {item['shareurl']}")
|
||||||
|
print(f"标签: {' '.join(item['tags'])}")
|
||||||
|
print("-" * 50)
|
||||||
16
app/sdk/common.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
def iso_to_cst(iso_time_str: str) -> str:
|
||||||
|
"""将 ISO 格式的时间字符串转换为 CST(China Standard Time) 时间并格式化为 %Y-%m-%d %H:%M:%S 格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
iso_time_str (str): ISO 格式时间字符串
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: CST(China Standard Time) 时间字符串
|
||||||
|
"""
|
||||||
|
dt = datetime.fromisoformat(iso_time_str)
|
||||||
|
tz = timezone(timedelta(hours=8))
|
||||||
|
dt_cst = dt if dt.astimezone(tz) > datetime.now(tz) else dt.astimezone(tz)
|
||||||
|
return dt_cst.strftime("%Y-%m-%d %H:%M:%S") if dt_cst.year >= 1970 else ""
|
||||||
97
app/sdk/pansou.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from sdk.common import iso_to_cst
|
||||||
|
|
||||||
|
|
||||||
|
class PanSou:
|
||||||
|
"""
|
||||||
|
PanSou 类,用于获取云盘资源
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, server):
|
||||||
|
self.server = server
|
||||||
|
self.session = requests.Session()
|
||||||
|
|
||||||
|
def search(self, keyword: str, refresh: bool = False) -> list:
|
||||||
|
"""搜索资源
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyword (str): 搜索关键字
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: 资源列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{self.server.rstrip('/')}/api/search"
|
||||||
|
params = {"kw": keyword, "cloud_types": ["quark"], "res": "merge", "refresh": refresh}
|
||||||
|
response = self.session.get(url, params=params)
|
||||||
|
result = response.json()
|
||||||
|
if result.get("code") == 0:
|
||||||
|
data = result.get("data", {}).get("merged_by_type", {}).get("quark", [])
|
||||||
|
return self.format_search_results(data)
|
||||||
|
return []
|
||||||
|
except Exception as _:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def format_search_results(self, search_results: list) -> list:
|
||||||
|
"""格式化搜索结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_results (list): 搜索结果列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: 夸克网盘资源列表
|
||||||
|
"""
|
||||||
|
pattern = (
|
||||||
|
r'^(.*?)'
|
||||||
|
r'(?:'
|
||||||
|
r'[【\[]?'
|
||||||
|
r'(?:简介|介绍|描述)'
|
||||||
|
r'[】\]]?'
|
||||||
|
r'[::]?'
|
||||||
|
r')'
|
||||||
|
r'(.*)$'
|
||||||
|
)
|
||||||
|
format_results = []
|
||||||
|
link_array = []
|
||||||
|
for item in search_results:
|
||||||
|
url = item.get("url", "")
|
||||||
|
note = item.get("note", "")
|
||||||
|
tm = item.get("datetime", "")
|
||||||
|
if tm:
|
||||||
|
tm = iso_to_cst(tm)
|
||||||
|
|
||||||
|
match = re.search(pattern, note)
|
||||||
|
if match:
|
||||||
|
title = match.group(1)
|
||||||
|
content = match.group(2)
|
||||||
|
else:
|
||||||
|
title = note
|
||||||
|
content = ""
|
||||||
|
|
||||||
|
if url != "" and url not in link_array:
|
||||||
|
link_array.append(url)
|
||||||
|
format_results.append({
|
||||||
|
"shareurl": url,
|
||||||
|
"taskname": title,
|
||||||
|
"content": content,
|
||||||
|
"datetime": tm,
|
||||||
|
"channel": item.get("source", ""),
|
||||||
|
"source": "PanSou"
|
||||||
|
})
|
||||||
|
|
||||||
|
return format_results
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server: str = "https://so.252035.xyz"
|
||||||
|
pansou = PanSou(server)
|
||||||
|
results = pansou.search("哪吒")
|
||||||
|
for item in results:
|
||||||
|
print(f"标题: {item['taskname']}")
|
||||||
|
print(f"描述: {item['content']}")
|
||||||
|
print(f"链接: {item['shareurl']}")
|
||||||
|
print(f"时间: {item['datetime']}")
|
||||||
|
print("-" * 50)
|
||||||
2018
app/static/css/bootstrap-icons.css
vendored
5
app/static/css/bootstrap-icons.min.css
vendored
Normal file
243
app/static/css/dashboard.css
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
body {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding-bottom: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container-fluid {
|
||||||
|
padding-right: 5px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1360px) {
|
||||||
|
.container-fluid {
|
||||||
|
max-width: 1360px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-buttons {
|
||||||
|
z-index: 99;
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 20px;
|
||||||
|
background-color: transparent;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-buttons button {
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 10px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
max-height: calc(100vh - 200px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-suggestions {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
transform: translate(0, -100%);
|
||||||
|
top: 0;
|
||||||
|
margin-top: -5px;
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
z-index: 1021;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sidebar
|
||||||
|
*/
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
/* Behind the navbar */
|
||||||
|
padding: 54px 0 0;
|
||||||
|
/* Height of navbar */
|
||||||
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sticky {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
height: calc(100vh - 54px);
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Scrollable contents if viewport is shorter than content. */
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
||||||
|
.sidebar-sticky {
|
||||||
|
position: -webkit-sticky;
|
||||||
|
position: sticky;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link {
|
||||||
|
font-size: medium;
|
||||||
|
color: #333;
|
||||||
|
padding: 10px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
/* 添加过渡效果 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link:hover {
|
||||||
|
background-color: #e0f0ff;
|
||||||
|
/* 改变背景颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link i {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-heading {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Navbar
|
||||||
|
*/
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-toggler {
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .form-control {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control-dark {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control-dark:focus {
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-bottom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 32px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-relative:hover .position-absolute {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode-tutorial {
|
||||||
|
display: none;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
bottom: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode-tutorial img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 80px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-width: 0 0 0 5px !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
animation: slideIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
border-left-color: #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
border-left-color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning {
|
||||||
|
border-left-color: #ffc107 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
border-left-color: #17a2b8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/static/img/qrcode_tutorial.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
286
app/static/js/qas.addtask.user.js
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name QAS一键推送助手
|
||||||
|
// @namespace https://github.com/Cp0204/quark-auto-save
|
||||||
|
// @license AGPL
|
||||||
|
// @version 0.6
|
||||||
|
// @description 在夸克网盘分享页面添加推送到 QAS 的按钮
|
||||||
|
// @icon https://pan.quark.cn/favicon.ico
|
||||||
|
// @author Cp0204
|
||||||
|
// @match https://pan.quark.cn/s/*
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
|
||||||
|
// @downloadURL https://cdn.jsdelivr.net/gh/Cp0204/quark-auto-save@refs/heads/main/app/static/js/qas.addtask.user.js
|
||||||
|
// @updateURL https://cdn.jsdelivr.net/gh/Cp0204/quark-auto-save@refs/heads/main/app/static/js/qas.addtask.user.js
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let qas_base = GM_getValue('qas_base', '');
|
||||||
|
let qas_token = GM_getValue('qas_token', '');
|
||||||
|
let default_pattern = GM_getValue('default_pattern', '');
|
||||||
|
let default_replace = GM_getValue('default_replace', '');
|
||||||
|
|
||||||
|
// QAS 设置弹窗函数
|
||||||
|
function showQASSettingDialog(callback) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'QAS 设置',
|
||||||
|
showCancelButton: true,
|
||||||
|
html: `
|
||||||
|
<label for="qas_base">QAS 地址</label>
|
||||||
|
<input id="qas_base" class="swal2-input" placeholder="如: http://192.168.1.8:5005" value="${qas_base}"><br>
|
||||||
|
<label for="qas_token">QAS Token</label>
|
||||||
|
<input id="qas_token" class="swal2-input" placeholder="v0.5+ 系统配置中查找" value="${qas_token}"><br>
|
||||||
|
<label for="qas_token">默认正则</label>
|
||||||
|
<input id="default_pattern" class="swal2-input" placeholder="如 $TV" value="${default_pattern}"><br>
|
||||||
|
<label for="qas_token">默认替换</label><input id="default_replace" class="swal2-input" value="${default_replace}">
|
||||||
|
`,
|
||||||
|
focusConfirm: false,
|
||||||
|
preConfirm: () => {
|
||||||
|
qas_base = document.getElementById('qas_base').value;
|
||||||
|
qas_token = document.getElementById('qas_token').value;
|
||||||
|
default_pattern = document.getElementById('default_pattern').value;
|
||||||
|
default_replace = document.getElementById('default_replace').value;
|
||||||
|
if (!qas_base || !qas_token) {
|
||||||
|
Swal.showValidationMessage('请填写 QAS 地址和 Token');
|
||||||
|
}
|
||||||
|
return { qas_base: qas_base, qas_token: qas_token, default_pattern: default_pattern, default_replace: default_replace }
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
GM_setValue('qas_base', result.value.qas_base);
|
||||||
|
GM_setValue('qas_token', result.value.qas_token);
|
||||||
|
GM_setValue('default_pattern', result.value.default_pattern);
|
||||||
|
GM_setValue('default_replace', result.value.default_replace);
|
||||||
|
qas_base = result.value.qas_base;
|
||||||
|
qas_token = result.value.qas_token;
|
||||||
|
default_pattern = result.value.default_pattern;
|
||||||
|
default_replace = result.value.default_replace;
|
||||||
|
if (callback) {
|
||||||
|
callback(); // 执行回调函数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 QAS 设置按钮
|
||||||
|
function addQASSettingButton() {
|
||||||
|
function waitForElement(selector, callback) {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
callback(element);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => waitForElement(selector, callback), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForElement('.pc-member-entrance', (PcMemberButton) => {
|
||||||
|
const qasSettingButton = document.createElement('div');
|
||||||
|
qasSettingButton.className = 'pc-member-entrance';
|
||||||
|
qasSettingButton.innerHTML = 'QAS设置';
|
||||||
|
|
||||||
|
qasSettingButton.addEventListener('click', () => {
|
||||||
|
showQASSettingDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
PcMemberButton.parentNode.insertBefore(qasSettingButton, PcMemberButton.nextSibling);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推送到 QAS 按钮
|
||||||
|
function addQASButton() {
|
||||||
|
function waitForElement(selector, callback) {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
callback(element);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => waitForElement(selector, callback), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForElement('.ant-btn.share-save', (saveButton) => {
|
||||||
|
const qasButton = document.createElement('button');
|
||||||
|
qasButton.type = 'button';
|
||||||
|
qasButton.className = 'ant-btn share-save';
|
||||||
|
qasButton.style.marginLeft = '10px';
|
||||||
|
qasButton.innerHTML = '<span class="share-save-ico"></span><span>创建QAS任务</span>';
|
||||||
|
|
||||||
|
let taskname, shareurl, savepath; // 声明变量
|
||||||
|
|
||||||
|
// 获取数据函数
|
||||||
|
function getData() {
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const lastTitle = document.querySelector('.primary .bcrumb-filename:last-child')?.getAttribute('title') || null;
|
||||||
|
taskname = (lastTitle && lastTitle != "全部文件") ? lastTitle : document.querySelector('.author-name').textContent;
|
||||||
|
shareurl = currentUrl;
|
||||||
|
let pathElement = document.querySelector('.path-name');
|
||||||
|
savepath = pathElement ? pathElement.title.replace('全部文件', '').trim() : "";
|
||||||
|
savepath += "/" + taskname;
|
||||||
|
qasButton.title = `任务名称: ${taskname}\n分享链接: ${shareurl}\n保存路径: ${savepath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 添加鼠标悬停事件
|
||||||
|
qasButton.addEventListener('mouseover', () => {
|
||||||
|
getData(); // 鼠标悬停时获取数据
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 添加点击事件
|
||||||
|
qasButton.addEventListener('click', () => {
|
||||||
|
getData(); // 点击时重新获取数据,确保最新
|
||||||
|
|
||||||
|
// 检查 qas_base 是否包含 http 或 https,如果没有则添加 http://
|
||||||
|
let qasApiBase = qas_base;
|
||||||
|
if (!qasApiBase.startsWith('http')) {
|
||||||
|
qasApiBase = 'http://' + qasApiBase;
|
||||||
|
}
|
||||||
|
const apiUrl = `${qasApiBase}/api/add_task?token=${qas_token}`;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"taskname": taskname,
|
||||||
|
"shareurl": shareurl,
|
||||||
|
"savepath": savepath,
|
||||||
|
"pattern": default_pattern,
|
||||||
|
"replace": default_replace,
|
||||||
|
};
|
||||||
|
|
||||||
|
GM_xmlhttpRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: apiUrl,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
onload: function (response) {
|
||||||
|
// 检查 HTTP 状态码
|
||||||
|
if (response.status === 401) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '认证失败',
|
||||||
|
text: 'Token 无效或已过期,请重新配置 QAS Token',
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: '重新配置',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
showQASSettingDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 503) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '服务器不可用',
|
||||||
|
html: `服务器暂时无法处理请求 (503)<br><br>
|
||||||
|
<small>可能原因:<br>
|
||||||
|
• QAS 服务未运行<br>
|
||||||
|
• 服务器过载<br>
|
||||||
|
• 网络连接问题</small>`,
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: '重新配置',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
showQASSettingDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查响应内容类型
|
||||||
|
const contentType = response.responseHeaders.match(/content-type:\s*([^;\s]+)/i);
|
||||||
|
if (contentType && !contentType[1].includes('application/json')) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '认证失败',
|
||||||
|
html: `服务器返回了非 JSON 响应,可能是 Token 错误<br><br>
|
||||||
|
<small>响应类型: ${contentType[1]}</small><br>
|
||||||
|
<small>响应状态: ${response.status}</small>`,
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: '重新配置',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
showQASSettingDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonResponse = JSON.parse(response.responseText);
|
||||||
|
if (jsonResponse.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '任务创建成功',
|
||||||
|
html: `<small>
|
||||||
|
<b>任务名称:</b> ${taskname}<br><br>
|
||||||
|
<b>保存路径:</b> ${savepath}<br><br>
|
||||||
|
<a href="${qasApiBase}" target="_blank">去 QAS 查看</a>
|
||||||
|
<small>`,
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
title: '任务创建失败',
|
||||||
|
text: jsonResponse.message,
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '解析响应失败',
|
||||||
|
html: `<small>
|
||||||
|
响应状态: ${response.status}<br>
|
||||||
|
响应内容: ${response.responseText.substring(0, 200)}...<br><br>
|
||||||
|
错误详情: ${e.message}
|
||||||
|
</small>`,
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: '重新配置',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
showQASSettingDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror: function (error) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '网络请求失败',
|
||||||
|
text: '无法连接到 QAS 服务器,请检查网络连接和服务器地址',
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: '重新配置',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
showQASSettingDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
saveButton.parentNode.insertBefore(qasButton, saveButton.nextSibling);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
(function init() {
|
||||||
|
addQASSettingButton();
|
||||||
|
|
||||||
|
if (!qas_base || !qas_token) {
|
||||||
|
showQASSettingDialog(() => {
|
||||||
|
addQASButton(); // 在设置后添加 QAS 按钮
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addQASButton(); // 如果配置存在,则直接添加 QAS 按钮
|
||||||
|
}
|
||||||
|
})(); // 立即执行初始化
|
||||||
|
})();
|
||||||
32
app/static/js/v-jsoneditor.min.js
vendored
Normal file
@ -1,33 +1,84 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>登录</title>
|
<title>登录</title>
|
||||||
<!-- 引入 Bootstrap CSS -->
|
|
||||||
<link rel="stylesheet" href="./static/css/bootstrap.min.css">
|
<link rel="stylesheet" href="./static/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="./static/css/bootstrap-icons.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #c4d7ff 0%, #7996ff 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-body {
|
||||||
|
background: #fff;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container mt-5">
|
<div class="container">
|
||||||
<h1 class="mb-4">登录</h1>
|
<div class="login-card">
|
||||||
{% if message %}
|
<div class="login-header">
|
||||||
<div class="alert alert-danger" role="alert">
|
<h1 class="mb-3">登录</h1>
|
||||||
[[ message ]]
|
<p class="text-muted">欢迎回来,请登录您的账户</p>
|
||||||
|
</div>
|
||||||
|
<div class="login-body">
|
||||||
|
{% if message %}
|
||||||
|
<div class="alert alert-danger text-center" role="alert">
|
||||||
|
[[ message ]]
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<form action="/login" method="POST">
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="username" class="form-label">用户名</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="bi bi-person-fill"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="password" class="form-label">密码</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">登录</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
<form action="/login" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">用户名</label>
|
|
||||||
<input type="text" class="form-control" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">密码</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">登录</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
BIN
img/run_log.png
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 313 KiB |
@ -1,7 +0,0 @@
|
|||||||
[
|
|
||||||
"alist",
|
|
||||||
"alist_strm",
|
|
||||||
"alist_strm_gen",
|
|
||||||
"emby",
|
|
||||||
"plex"
|
|
||||||
]
|
|
||||||
252
notify.py
@ -33,7 +33,7 @@ def print(text, *args, **kw):
|
|||||||
# 通知服务
|
# 通知服务
|
||||||
# fmt: off
|
# fmt: off
|
||||||
push_config = {
|
push_config = {
|
||||||
'HITOKOTO': True, # 启用一言(随机句子)
|
'HITOKOTO': False, # 启用一言(随机句子)
|
||||||
|
|
||||||
'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
|
'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
|
||||||
'BARK_ARCHIVE': '', # bark 推送是否存档
|
'BARK_ARCHIVE': '', # bark 推送是否存档
|
||||||
@ -43,7 +43,7 @@ push_config = {
|
|||||||
'BARK_LEVEL': '', # bark 推送时效性
|
'BARK_LEVEL': '', # bark 推送时效性
|
||||||
'BARK_URL': '', # bark 推送跳转URL
|
'BARK_URL': '', # bark 推送跳转URL
|
||||||
|
|
||||||
'CONSOLE': False, # 控制台输出
|
'CONSOLE': False, # 控制台输出
|
||||||
|
|
||||||
'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
|
'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
|
||||||
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
|
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
|
||||||
@ -72,12 +72,17 @@ push_config = {
|
|||||||
'CHAT_URL': '', # synology chat url
|
'CHAT_URL': '', # synology chat url
|
||||||
'CHAT_TOKEN': '', # synology chat token
|
'CHAT_TOKEN': '', # synology chat token
|
||||||
|
|
||||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
'PUSH_PLUS_TOKEN': '', # pushplus 推送的用户令牌
|
||||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
'PUSH_PLUS_USER': '', # pushplus 推送的群组编码
|
||||||
|
'PUSH_PLUS_TEMPLATE': 'html', # pushplus 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
|
||||||
|
'PUSH_PLUS_CHANNEL': 'wechat', # pushplus 发送渠道,支持wechat,webhook,cp,mail,sms
|
||||||
|
'PUSH_PLUS_WEBHOOK': '', # pushplus webhook编码,可在pushplus公众号上扩展配置出更多渠道
|
||||||
|
'PUSH_PLUS_CALLBACKURL': '', # pushplus 发送结果回调地址,会把推送最终结果通知到这个地址上
|
||||||
|
'PUSH_PLUS_TO': '', # pushplus 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
|
||||||
|
|
||||||
'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
|
'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
|
||||||
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
|
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
|
||||||
'WE_PLUS_BOT_VERSION': 'pro', # 微加机器人的调用版本
|
'WE_PLUS_BOT_VERSION': 'pro', # 微加机器人的调用版本
|
||||||
|
|
||||||
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
|
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
|
||||||
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
||||||
@ -101,9 +106,11 @@ push_config = {
|
|||||||
|
|
||||||
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||||
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
|
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
|
||||||
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
|
'SMTP_EMAIL': '', # SMTP 发件邮箱
|
||||||
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||||
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
'SMTP_NAME': '', # SMTP 发件人姓名,可随意填写
|
||||||
|
'SMTP_EMAIL_TO': '', # SMTP 收件邮箱,可选,缺省时将自己发给自己,多个收件邮箱逗号间隔
|
||||||
|
'SMTP_NAME_TO': '', # SMTP 收件人姓名,可选,可随意填写,多个收件人逗号间隔,顺序与 SMTP_EMAIL_TO 保持一致
|
||||||
|
|
||||||
'PUSHME_KEY': '', # PushMe 的 PUSHME_KEY
|
'PUSHME_KEY': '', # PushMe 的 PUSHME_KEY
|
||||||
'PUSHME_URL': '', # PushMe 的 PUSHME_URL
|
'PUSHME_URL': '', # PushMe 的 PUSHME_URL
|
||||||
@ -121,6 +128,19 @@ push_config = {
|
|||||||
'NTFY_URL': '', # ntfy地址,如https://ntfy.sh
|
'NTFY_URL': '', # ntfy地址,如https://ntfy.sh
|
||||||
'NTFY_TOPIC': '', # ntfy的消息应用topic
|
'NTFY_TOPIC': '', # ntfy的消息应用topic
|
||||||
'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3
|
'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3
|
||||||
|
'NTFY_TOKEN': '', # 推送token,可选
|
||||||
|
'NTFY_USERNAME': '', # 推送用户名称,可选
|
||||||
|
'NTFY_PASSWORD': '', # 推送用户密码,可选
|
||||||
|
'NTFY_ACTIONS': '', # 推送用户动作,可选
|
||||||
|
|
||||||
|
'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||||
|
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
|
'WXPUSHER_UIDS': '', # wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
|
|
||||||
|
'DODO_BOTTOKEN': '', # DoDo机器人的token DoDo开发平台https://doker.imdodo.com/
|
||||||
|
'DODO_BOTID': '', # DoDo机器人的id
|
||||||
|
'DODO_LANDSOURCEID': '', # DoDo机器人所在的群ID
|
||||||
|
'DODO_SOURCEID': '', # DoDo机器人推送目标用户的ID
|
||||||
}
|
}
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
@ -135,7 +155,6 @@ def bark(title: str, content: str) -> None:
|
|||||||
使用 bark 推送消息。
|
使用 bark 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("BARK_PUSH"):
|
if not push_config.get("BARK_PUSH"):
|
||||||
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("bark 服务启动")
|
print("bark 服务启动")
|
||||||
|
|
||||||
@ -179,7 +198,8 @@ def console(title: str, content: str) -> None:
|
|||||||
"""
|
"""
|
||||||
使用 控制台 推送消息。
|
使用 控制台 推送消息。
|
||||||
"""
|
"""
|
||||||
print(f"{title}\n\n{content}")
|
if str(push_config.get("CONSOLE")).lower() != "false":
|
||||||
|
print(f"{title}\n\n{content}")
|
||||||
|
|
||||||
|
|
||||||
def dingding_bot(title: str, content: str) -> None:
|
def dingding_bot(title: str, content: str) -> None:
|
||||||
@ -187,7 +207,6 @@ def dingding_bot(title: str, content: str) -> None:
|
|||||||
使用 钉钉机器人 推送消息。
|
使用 钉钉机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
|
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
|
||||||
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("钉钉机器人 服务启动")
|
print("钉钉机器人 服务启动")
|
||||||
|
|
||||||
@ -217,7 +236,6 @@ def feishu_bot(title: str, content: str) -> None:
|
|||||||
使用 飞书机器人 推送消息。
|
使用 飞书机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("FSKEY"):
|
if not push_config.get("FSKEY"):
|
||||||
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("飞书 服务启动")
|
print("飞书 服务启动")
|
||||||
|
|
||||||
@ -236,7 +254,6 @@ def go_cqhttp(title: str, content: str) -> None:
|
|||||||
使用 go_cqhttp 推送消息。
|
使用 go_cqhttp 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
|
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
|
||||||
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("go-cqhttp 服务启动")
|
print("go-cqhttp 服务启动")
|
||||||
|
|
||||||
@ -254,7 +271,6 @@ def gotify(title: str, content: str) -> None:
|
|||||||
使用 gotify 推送消息。
|
使用 gotify 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
||||||
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("gotify 服务启动")
|
print("gotify 服务启动")
|
||||||
|
|
||||||
@ -277,7 +293,6 @@ def iGot(title: str, content: str) -> None:
|
|||||||
使用 iGot 推送消息。
|
使用 iGot 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("IGOT_PUSH_KEY"):
|
if not push_config.get("IGOT_PUSH_KEY"):
|
||||||
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("iGot 服务启动")
|
print("iGot 服务启动")
|
||||||
|
|
||||||
@ -297,13 +312,12 @@ def serverJ(title: str, content: str) -> None:
|
|||||||
通过 serverJ 推送消息。
|
通过 serverJ 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("PUSH_KEY"):
|
if not push_config.get("PUSH_KEY"):
|
||||||
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("serverJ 服务启动")
|
print("serverJ 服务启动")
|
||||||
|
|
||||||
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
||||||
|
|
||||||
match = re.match(r'sctp(\d+)t', push_config.get("PUSH_KEY"))
|
match = re.match(r"sctp(\d+)t", push_config.get("PUSH_KEY"))
|
||||||
if match:
|
if match:
|
||||||
num = match.group(1)
|
num = match.group(1)
|
||||||
url = f'https://{num}.push.ft07.com/send/{push_config.get("PUSH_KEY")}.send'
|
url = f'https://{num}.push.ft07.com/send/{push_config.get("PUSH_KEY")}.send'
|
||||||
@ -323,7 +337,6 @@ def pushdeer(title: str, content: str) -> None:
|
|||||||
通过PushDeer 推送消息
|
通过PushDeer 推送消息
|
||||||
"""
|
"""
|
||||||
if not push_config.get("DEER_KEY"):
|
if not push_config.get("DEER_KEY"):
|
||||||
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("PushDeer 服务启动")
|
print("PushDeer 服务启动")
|
||||||
data = {
|
data = {
|
||||||
@ -349,7 +362,6 @@ def chat(title: str, content: str) -> None:
|
|||||||
通过Chat 推送消息
|
通过Chat 推送消息
|
||||||
"""
|
"""
|
||||||
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
|
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
|
||||||
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("chat 服务启动")
|
print("chat 服务启动")
|
||||||
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
||||||
@ -364,26 +376,36 @@ def chat(title: str, content: str) -> None:
|
|||||||
|
|
||||||
def pushplus_bot(title: str, content: str) -> None:
|
def pushplus_bot(title: str, content: str) -> None:
|
||||||
"""
|
"""
|
||||||
通过 push+ 推送消息。
|
通过 pushplus 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("PUSH_PLUS_TOKEN"):
|
if not push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("PUSHPLUS 服务启动")
|
print("PUSHPLUS 服务启动")
|
||||||
|
|
||||||
url = "http://www.pushplus.plus/send"
|
url = "https://www.pushplus.plus/send"
|
||||||
data = {
|
data = {
|
||||||
"token": push_config.get("PUSH_PLUS_TOKEN"),
|
"token": push_config.get("PUSH_PLUS_TOKEN"),
|
||||||
"title": title,
|
"title": title,
|
||||||
"content": content,
|
"content": content,
|
||||||
"topic": push_config.get("PUSH_PLUS_USER"),
|
"topic": push_config.get("PUSH_PLUS_USER"),
|
||||||
|
"template": push_config.get("PUSH_PLUS_TEMPLATE"),
|
||||||
|
"channel": push_config.get("PUSH_PLUS_CHANNEL"),
|
||||||
|
"webhook": push_config.get("PUSH_PLUS_WEBHOOK"),
|
||||||
|
"callbackUrl": push_config.get("PUSH_PLUS_CALLBACKURL"),
|
||||||
|
"to": push_config.get("PUSH_PLUS_TO"),
|
||||||
}
|
}
|
||||||
body = json.dumps(data).encode(encoding="utf-8")
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
response = requests.post(url=url, data=body, headers=headers).json()
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
|
|
||||||
if response["code"] == 200:
|
code = response["code"]
|
||||||
print("PUSHPLUS 推送成功!")
|
if code == 200:
|
||||||
|
print("PUSHPLUS 推送请求成功,可根据流水号查询推送结果:" + response["data"])
|
||||||
|
print(
|
||||||
|
"注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果"
|
||||||
|
)
|
||||||
|
elif code == 900 or code == 903 or code == 905 or code == 999:
|
||||||
|
print(response["msg"])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
url_old = "http://pushplus.hxtrip.com/send"
|
url_old = "http://pushplus.hxtrip.com/send"
|
||||||
@ -402,7 +424,6 @@ def weplus_bot(title: str, content: str) -> None:
|
|||||||
通过 微加机器人 推送消息。
|
通过 微加机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("WE_PLUS_BOT_TOKEN"):
|
if not push_config.get("WE_PLUS_BOT_TOKEN"):
|
||||||
print("微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("微加机器人 服务启动")
|
print("微加机器人 服务启动")
|
||||||
|
|
||||||
@ -434,7 +455,6 @@ def qmsg_bot(title: str, content: str) -> None:
|
|||||||
使用 qmsg 推送消息。
|
使用 qmsg 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
|
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
|
||||||
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("qmsg 服务启动")
|
print("qmsg 服务启动")
|
||||||
|
|
||||||
@ -453,11 +473,10 @@ def wecom_app(title: str, content: str) -> None:
|
|||||||
通过 企业微信 APP 推送消息。
|
通过 企业微信 APP 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("QYWX_AM"):
|
if not push_config.get("QYWX_AM"):
|
||||||
print("QYWX_AM 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
||||||
if 4 < len(QYWX_AM_AY) > 5:
|
if 4 < len(QYWX_AM_AY) > 5:
|
||||||
print("QYWX_AM 设置错误!!\n取消推送")
|
print("QYWX_AM 设置错误!!")
|
||||||
return
|
return
|
||||||
print("企业微信 APP 服务启动")
|
print("企业微信 APP 服务启动")
|
||||||
|
|
||||||
@ -550,7 +569,6 @@ def wecom_bot(title: str, content: str) -> None:
|
|||||||
通过 企业微信机器人 推送消息。
|
通过 企业微信机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("QYWX_KEY"):
|
if not push_config.get("QYWX_KEY"):
|
||||||
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("企业微信机器人服务启动")
|
print("企业微信机器人服务启动")
|
||||||
|
|
||||||
@ -576,7 +594,6 @@ def telegram_bot(title: str, content: str) -> None:
|
|||||||
使用 telegram 机器人 推送消息。
|
使用 telegram 机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
|
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
|
||||||
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("tg 服务启动")
|
print("tg 服务启动")
|
||||||
|
|
||||||
@ -625,9 +642,6 @@ def aibotk(title: str, content: str) -> None:
|
|||||||
or not push_config.get("AIBOTK_TYPE")
|
or not push_config.get("AIBOTK_TYPE")
|
||||||
or not push_config.get("AIBOTK_NAME")
|
or not push_config.get("AIBOTK_NAME")
|
||||||
):
|
):
|
||||||
print(
|
|
||||||
"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
print("智能微秘书 服务启动")
|
print("智能微秘书 服务启动")
|
||||||
|
|
||||||
@ -666,9 +680,6 @@ def smtp(title: str, content: str) -> None:
|
|||||||
or not push_config.get("SMTP_PASSWORD")
|
or not push_config.get("SMTP_PASSWORD")
|
||||||
or not push_config.get("SMTP_NAME")
|
or not push_config.get("SMTP_NAME")
|
||||||
):
|
):
|
||||||
print(
|
|
||||||
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
print("SMTP 邮件 服务启动")
|
print("SMTP 邮件 服务启动")
|
||||||
|
|
||||||
@ -679,12 +690,23 @@ def smtp(title: str, content: str) -> None:
|
|||||||
push_config.get("SMTP_EMAIL"),
|
push_config.get("SMTP_EMAIL"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
message["To"] = formataddr(
|
if not push_config.get("SMTP_EMAIL_TO"):
|
||||||
(
|
smtp_email_to = push_config.get("SMTP_EMAIL")
|
||||||
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
|
message["To"] = formataddr(
|
||||||
push_config.get("SMTP_EMAIL"),
|
(
|
||||||
|
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
|
||||||
|
push_config.get("SMTP_EMAIL"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
else:
|
||||||
|
smtp_email_to = push_config.get("SMTP_EMAIL_TO").split(",")
|
||||||
|
smtp_name_to = push_config.get("SMTP_NAME_TO","").split(",")
|
||||||
|
message["To"] = ",".join([formataddr(
|
||||||
|
(
|
||||||
|
Header(smtp_name_to[i] if len(smtp_name_to) > i else "", "utf-8").encode(),
|
||||||
|
email_to,
|
||||||
|
)
|
||||||
|
) for i, email_to in enumerate(smtp_email_to)])
|
||||||
message["Subject"] = Header(title, "utf-8")
|
message["Subject"] = Header(title, "utf-8")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -698,7 +720,7 @@ def smtp(title: str, content: str) -> None:
|
|||||||
)
|
)
|
||||||
smtp_server.sendmail(
|
smtp_server.sendmail(
|
||||||
push_config.get("SMTP_EMAIL"),
|
push_config.get("SMTP_EMAIL"),
|
||||||
push_config.get("SMTP_EMAIL"),
|
smtp_email_to,
|
||||||
message.as_bytes(),
|
message.as_bytes(),
|
||||||
)
|
)
|
||||||
smtp_server.close()
|
smtp_server.close()
|
||||||
@ -712,7 +734,6 @@ def pushme(title: str, content: str) -> None:
|
|||||||
使用 PushMe 推送消息。
|
使用 PushMe 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("PUSHME_KEY"):
|
if not push_config.get("PUSHME_KEY"):
|
||||||
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("PushMe 服务启动")
|
print("PushMe 服务启动")
|
||||||
|
|
||||||
@ -745,7 +766,6 @@ def chronocat(title: str, content: str) -> None:
|
|||||||
or not push_config.get("CHRONOCAT_QQ")
|
or not push_config.get("CHRONOCAT_QQ")
|
||||||
or not push_config.get("CHRONOCAT_TOKEN")
|
or not push_config.get("CHRONOCAT_TOKEN")
|
||||||
):
|
):
|
||||||
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print("CHRONOCAT 服务启动")
|
print("CHRONOCAT 服务启动")
|
||||||
@ -789,17 +809,17 @@ def ntfy(title: str, content: str) -> None:
|
|||||||
"""
|
"""
|
||||||
通过 Ntfy 推送消息
|
通过 Ntfy 推送消息
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def encode_rfc2047(text: str) -> str:
|
def encode_rfc2047(text: str) -> str:
|
||||||
"""将文本编码为符合 RFC 2047 标准的格式"""
|
"""将文本编码为符合 RFC 2047 标准的格式"""
|
||||||
encoded_bytes = base64.b64encode(text.encode('utf-8'))
|
encoded_bytes = base64.b64encode(text.encode("utf-8"))
|
||||||
encoded_str = encoded_bytes.decode('utf-8')
|
encoded_str = encoded_bytes.decode("utf-8")
|
||||||
return f'=?utf-8?B?{encoded_str}?='
|
return f"=?utf-8?B?{encoded_str}?="
|
||||||
|
|
||||||
if not push_config.get("NTFY_TOPIC"):
|
if not push_config.get("NTFY_TOPIC"):
|
||||||
print("ntfy 服务的 NTFY_TOPIC 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
print("ntfy 服务启动")
|
print("ntfy 服务启动")
|
||||||
priority = '3'
|
priority = "3"
|
||||||
if not push_config.get("NTFY_PRIORITY"):
|
if not push_config.get("NTFY_PRIORITY"):
|
||||||
print("ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3")
|
print("ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3")
|
||||||
else:
|
else:
|
||||||
@ -808,11 +828,15 @@ def ntfy(title: str, content: str) -> None:
|
|||||||
# 使用 RFC 2047 编码 title
|
# 使用 RFC 2047 编码 title
|
||||||
encoded_title = encode_rfc2047(title)
|
encoded_title = encode_rfc2047(title)
|
||||||
|
|
||||||
data = content.encode(encoding='utf-8')
|
data = content.encode(encoding="utf-8")
|
||||||
headers = {
|
headers = {"Title": encoded_title, "Priority": priority, "Icon": "https://qn.whyour.cn/logo.png"} # 使用编码后的 title
|
||||||
"Title": encoded_title, # 使用编码后的 title
|
if push_config.get("NTFY_TOKEN"):
|
||||||
"Priority": priority
|
headers['Authorization'] = "Bearer " + push_config.get("NTFY_TOKEN")
|
||||||
}
|
elif push_config.get("NTFY_USERNAME") and push_config.get("NTFY_PASSWORD"):
|
||||||
|
authStr = push_config.get("NTFY_USERNAME") + ":" + push_config.get("NTFY_PASSWORD")
|
||||||
|
headers['Authorization'] = "Basic " + base64.b64encode(authStr.encode('utf-8')).decode('utf-8')
|
||||||
|
if push_config.get("NTFY_ACTIONS"):
|
||||||
|
headers['Actions'] = encode_rfc2047(push_config.get("NTFY_ACTIONS"))
|
||||||
|
|
||||||
url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC")
|
url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC")
|
||||||
response = requests.post(url, data=data, headers=headers)
|
response = requests.post(url, data=data, headers=headers)
|
||||||
@ -821,6 +845,111 @@ def ntfy(title: str, content: str) -> None:
|
|||||||
else:
|
else:
|
||||||
print("Ntfy 推送失败!错误信息:", response.text)
|
print("Ntfy 推送失败!错误信息:", response.text)
|
||||||
|
|
||||||
|
def dodo_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 DoDo机器人 推送消息
|
||||||
|
"""
|
||||||
|
required_keys = [
|
||||||
|
'DODO_BOTTOKEN',
|
||||||
|
'DODO_BOTID',
|
||||||
|
'DODO_LANDSOURCEID',
|
||||||
|
'DODO_SOURCEID'
|
||||||
|
]
|
||||||
|
if not all(push_config.get(key) for key in required_keys):
|
||||||
|
missing = [key for key in required_keys if not push_config.get(key)]
|
||||||
|
print(f"DoDo 服务配置不完整,缺少以下参数: {', '.join(missing)}\n取消推送")
|
||||||
|
return
|
||||||
|
print("DoDo 服务启动")
|
||||||
|
url="https://botopen.imdodo.com/api/v2/personal/message/send"
|
||||||
|
|
||||||
|
botID=push_config.get('DODO_BOTID')
|
||||||
|
botToken=push_config.get('DODO_BOTTOKEN')
|
||||||
|
islandSourceId=push_config.get('DODO_LANDSOURCEID')
|
||||||
|
dodoSourceId=push_config.get('DODO_SOURCEID')
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bot {botID}.{botToken}',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Host': 'botopen.imdodo.com'
|
||||||
|
}
|
||||||
|
payload = json.dumps({
|
||||||
|
"islandSourceId": islandSourceId,
|
||||||
|
"dodoSourceId": dodoSourceId,
|
||||||
|
"messageType": 1,
|
||||||
|
"messageBody": {
|
||||||
|
"content": f"{title}\n\n{content}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, data=payload)
|
||||||
|
if response.status_code == 200:
|
||||||
|
response = response.json()
|
||||||
|
if response.get("status") == 0 and response.get("message") == "success":
|
||||||
|
print(f'DoDo 推送成功!')
|
||||||
|
else:
|
||||||
|
print(f'DoDo 推送失败!错误信息:\n{response}')
|
||||||
|
else:
|
||||||
|
print("DoDo 推送失败!错误信息:", response.text)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DoDo 推送请求异常: {str(e)}")
|
||||||
|
|
||||||
|
def wxpusher_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 wxpusher 推送消息。
|
||||||
|
支持的环境变量:
|
||||||
|
- WXPUSHER_APP_TOKEN: appToken
|
||||||
|
- WXPUSHER_TOPIC_IDS: 主题ID, 多个用英文分号;分隔
|
||||||
|
- WXPUSHER_UIDS: 用户ID, 多个用英文分号;分隔
|
||||||
|
"""
|
||||||
|
if not push_config.get("WXPUSHER_APP_TOKEN"):
|
||||||
|
return
|
||||||
|
|
||||||
|
url = "https://wxpusher.zjiecode.com/api/send/message"
|
||||||
|
|
||||||
|
# 处理topic_ids和uids,将分号分隔的字符串转为数组
|
||||||
|
topic_ids = []
|
||||||
|
if push_config.get("WXPUSHER_TOPIC_IDS"):
|
||||||
|
topic_ids = [
|
||||||
|
int(id.strip())
|
||||||
|
for id in push_config.get("WXPUSHER_TOPIC_IDS").split(";")
|
||||||
|
if id.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
uids = []
|
||||||
|
if push_config.get("WXPUSHER_UIDS"):
|
||||||
|
uids = [
|
||||||
|
uid.strip()
|
||||||
|
for uid in push_config.get("WXPUSHER_UIDS").split(";")
|
||||||
|
if uid.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
# topic_ids uids 至少有一个
|
||||||
|
if not topic_ids and not uids:
|
||||||
|
print("wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("wxpusher 服务启动")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"appToken": push_config.get("WXPUSHER_APP_TOKEN"),
|
||||||
|
"content": f"<h1>{title}</h1><br/><div style='white-space: pre-wrap;'>{content}</div>",
|
||||||
|
"summary": title,
|
||||||
|
"contentType": 2,
|
||||||
|
"topicIds": topic_ids,
|
||||||
|
"uids": uids,
|
||||||
|
"verifyPayType": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
response = requests.post(url=url, json=data, headers=headers).json()
|
||||||
|
|
||||||
|
if response.get("code") == 1000:
|
||||||
|
print("wxpusher 推送成功!")
|
||||||
|
else:
|
||||||
|
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
|
||||||
|
|
||||||
|
|
||||||
def parse_headers(headers):
|
def parse_headers(headers):
|
||||||
if not headers:
|
if not headers:
|
||||||
return {}
|
return {}
|
||||||
@ -877,7 +1006,6 @@ def custom_notify(title: str, content: str) -> None:
|
|||||||
通过 自定义通知 推送消息。
|
通过 自定义通知 推送消息。
|
||||||
"""
|
"""
|
||||||
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
|
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
|
||||||
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print("自定义通知服务启动")
|
print("自定义通知服务启动")
|
||||||
@ -979,10 +1107,21 @@ def add_notify_function():
|
|||||||
and push_config.get("CHRONOCAT_TOKEN")
|
and push_config.get("CHRONOCAT_TOKEN")
|
||||||
):
|
):
|
||||||
notify_function.append(chronocat)
|
notify_function.append(chronocat)
|
||||||
|
if (
|
||||||
|
push_config.get("DODO_BOTTOKEN")
|
||||||
|
and push_config.get("DODO_BOTID")
|
||||||
|
and push_config.get("DODO_LANDSOURCEID")
|
||||||
|
and push_config.get("DODO_SOURCEID")
|
||||||
|
):
|
||||||
|
notify_function.append(dodo_bot)
|
||||||
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
|
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
|
||||||
notify_function.append(custom_notify)
|
notify_function.append(custom_notify)
|
||||||
if push_config.get("NTFY_TOPIC"):
|
if push_config.get("NTFY_TOPIC"):
|
||||||
notify_function.append(ntfy)
|
notify_function.append(ntfy)
|
||||||
|
if push_config.get("WXPUSHER_APP_TOKEN") and (
|
||||||
|
push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS")
|
||||||
|
):
|
||||||
|
notify_function.append(wxpusher_bot)
|
||||||
if not notify_function:
|
if not notify_function:
|
||||||
print(f"无推送渠道,请检查通知变量是否正确")
|
print(f"无推送渠道,请检查通知变量是否正确")
|
||||||
return notify_function
|
return notify_function
|
||||||
@ -1007,8 +1146,9 @@ def send(title: str, content: str, ignore_default_config: bool = False, **kwargs
|
|||||||
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
|
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
|
||||||
return
|
return
|
||||||
|
|
||||||
hitokoto = push_config.get("HITOKOTO", "false")
|
hitokoto = push_config.get("HITOKOTO")
|
||||||
content += "\n\n" + one() if hitokoto != "false" else ""
|
if hitokoto and str(hitokoto).lower() != "false":
|
||||||
|
content += "\n\n" + one()
|
||||||
|
|
||||||
notify_function = add_notify_function()
|
notify_function = add_notify_function()
|
||||||
ts = [
|
ts = [
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
# 媒体库模块开发指南
|
# 插件开发指南
|
||||||
|
|
||||||
本指南介绍如何开发自定义媒体库模块,你可以通过添加新的媒体库模块来扩展项目功能。
|
本指南介绍如何开发自定义插件,你可以通过添加新的插件来扩展项目功能。
|
||||||
|
|
||||||
## 基本结构
|
## 基本结构
|
||||||
|
|
||||||
* 模块位于 `media_servers` 目录下.
|
* 插件位于 `plugins` 目录下.
|
||||||
* 每个模块是一个 `.py` 文件 (例如 `emby.py`, `plex.py`),文件名小写。
|
* 每个插件是一个 `.py` 文件 (例如 `emby.py`, `plex.py`),文件名小写。
|
||||||
* 每个模块文件包含一个与文件名对应的首字母大写命名类(例如 `emby.py` 中的 `Emby` 类)。
|
* 每个插件文件包含一个与文件名对应的首字母大写命名类(例如 `emby.py` 中的 `Emby` 类)。
|
||||||
|
|
||||||
## 模块要求
|
## 插件要求
|
||||||
|
|
||||||
每个模块类必须包含以下内容:
|
每个插件类必须包含以下内容:
|
||||||
|
|
||||||
* **`default_config`**:字典,包含模块所需参数及其默认值。例如:
|
* **`default_config`**:字典,包含插件所需参数及其默认值。例如:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# 该模块必须配置的键,值可留空
|
# 该插件必须配置的键,值可留空
|
||||||
default_config = {"url": "", "token": ""}
|
default_config = {"url": "", "token": ""}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -25,11 +25,11 @@
|
|||||||
1. 检查 `kwargs` 是否包含所有 `default_config` 中的参数,缺少参数则打印警告。
|
1. 检查 `kwargs` 是否包含所有 `default_config` 中的参数,缺少参数则打印警告。
|
||||||
2. 若参数完整,尝试连接服务器并验证配置,成功则设置 `self.is_active = True`。
|
2. 若参数完整,尝试连接服务器并验证配置,成功则设置 `self.is_active = True`。
|
||||||
|
|
||||||
* **`run(self, task)`**:整个模块入口函数,处理模块逻辑。
|
* **`run(self, task, **kwargs)`**:整个插件入口函数,处理插件逻辑。
|
||||||
* `task` 是一个字典,包含任务信息。如果需要修改任务参数,返回修改后的 `task` 字典;
|
* `task` 是一个字典,包含任务信息。如果需要修改任务参数,返回修改后的 `task` 字典;
|
||||||
* 无修改则不返回或返回 `None`。
|
* 无修改则不返回或返回 `None`。
|
||||||
|
|
||||||
## 模块示例
|
## 插件示例
|
||||||
|
|
||||||
参考 [emby.py](emby.py)
|
参考 [emby.py](emby.py)
|
||||||
|
|
||||||
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
### 最佳实践
|
### 最佳实践
|
||||||
|
|
||||||
requests 部分使用 try-except 块,以防模块请求出错中断整个转存任务。
|
requests 部分使用 try-except 块,以防插件请求出错中断整个转存任务。
|
||||||
|
|
||||||
```python
|
```python
|
||||||
try:
|
try:
|
||||||
@ -51,31 +51,31 @@ try:
|
|||||||
# 处理响应数据
|
# 处理响应数据
|
||||||
# ......
|
# ......
|
||||||
# 返回
|
# 返回
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用自定义模块
|
## 使用自定义插件
|
||||||
|
|
||||||
放到 `/media_servers` 目录即可识别,如果你使用 docker 运行:
|
放到 `/plugins` 目录即可识别,如果你使用 docker 运行:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d \
|
docker run -d \
|
||||||
# ... 例如添加这行挂载,其它一致
|
# ... 例如添加这行挂载,其它一致
|
||||||
-v ./quark-auto-save/media_servers/plex.py:/app/media_servers/plex.py \
|
-v ./quark-auto-save/plugins/plex.py:/app/plugins/plex.py \
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
如果你有写自定义模块的能力,相信你也知道如何挂载自定义模块,算我啰嗦。🙃
|
如果你有写自定义插件的能力,相信你也知道如何挂载自定义插件,算我啰嗦。🙃
|
||||||
|
|
||||||
## 配置文件
|
## 配置文件
|
||||||
|
|
||||||
在 `quark_config.json` 的 `media_servers` 中配置模块参数:
|
在 `quark_config.json` 的 `plugins` 中配置插件参数:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"media_servers": {
|
"plugins": {
|
||||||
"emby": {
|
"emby": {
|
||||||
"url": "http://your-emby-server:8096",
|
"url": "http://your-emby-server:8096",
|
||||||
"token": "YOUR_EMBY_TOKEN"
|
"token": "YOUR_EMBY_TOKEN"
|
||||||
@ -84,10 +84,12 @@ docker run -d \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
当模块代码正确赋值 `default_config` 时,首次运行会自动补充缺失的键。
|
当插件代码正确赋值 `default_config` 时,首次运行会自动补充缺失的键。
|
||||||
|
|
||||||
## 🤝 贡献者
|
## 🤝 贡献者
|
||||||
|
|
||||||
| 模块 | 说明 | 贡献者 |
|
| 插件 | 说明 | 贡献者 |
|
||||||
| ------- | -------------------- | --------------------------------------- |
|
| ------- | -------------------- | --------------------------------------- |
|
||||||
| plex.py | 自动刷新 Plex 媒体库 | [zhazhayu](https://github.com/zhazhayu) |
|
| plex.py | 自动刷新 Plex 媒体库 | [zhazhayu](https://github.com/zhazhayu) |
|
||||||
|
| alist_strm_gen.py | 自动生成strm | [xiaoQQya](https://github.com/xiaoQQya) |
|
||||||
|
| alist_sync.py | 调用 alist 实现跨网盘转存 | [jenfonro](https://github.com/jenfonro) |
|
||||||
12
plugins/_priority.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
"smartstrm",
|
||||||
|
"fnv_refresh_v2",
|
||||||
|
"alist",
|
||||||
|
"alist_strm",
|
||||||
|
"alist_strm_gen",
|
||||||
|
"alist_sync",
|
||||||
|
"aria2",
|
||||||
|
"emby",
|
||||||
|
"plex",
|
||||||
|
"fnv"
|
||||||
|
]
|
||||||
@ -30,7 +30,7 @@ class Alist:
|
|||||||
self.storage_mount_path, self.quark_root_dir = result
|
self.storage_mount_path, self.quark_root_dir = result
|
||||||
self.is_active = True
|
self.is_active = True
|
||||||
|
|
||||||
def run(self, task):
|
def run(self, task, **kwargs):
|
||||||
if task.get("savepath") and task.get("savepath").startswith(
|
if task.get("savepath") and task.get("savepath").startswith(
|
||||||
self.quark_root_dir
|
self.quark_root_dir
|
||||||
):
|
):
|
||||||
@ -62,25 +62,39 @@ class Alist:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def storage_id_to_path(self, storage_id):
|
def storage_id_to_path(self, storage_id):
|
||||||
|
storage_mount_path, quark_root_dir = None, None
|
||||||
# 1. 检查是否符合 /aaa:/bbb 格式
|
# 1. 检查是否符合 /aaa:/bbb 格式
|
||||||
match = re.match(r"^(\/[^:]*):(\/[^:]*)$", storage_id)
|
if match := re.match(r"^(\/[^:]*):(\/[^:]*)$", storage_id):
|
||||||
if match:
|
# 存储挂载路径, 夸克根文件夹
|
||||||
return True, (match.group(1), match.group(2))
|
storage_mount_path, quark_root_dir = match.group(1), match.group(2)
|
||||||
# 2. 调用 Alist API 获取存储信息
|
file_list = self.get_file_list(storage_mount_path)
|
||||||
storage_info = self.get_storage_info(storage_id)
|
if file_list.get("code") != 200:
|
||||||
if storage_info:
|
print(f"Alist刷新: 获取挂载路径失败❌ {file_list.get('message')}")
|
||||||
if storage_info["driver"] == "Quark":
|
return False, (None, None)
|
||||||
addition = json.loads(storage_info["addition"])
|
# 2. 检查是否数字,调用 Alist API 获取存储信息
|
||||||
# 存储挂载路径
|
elif re.match(r"^\d+$", storage_id):
|
||||||
storage_mount_path = storage_info["mount_path"]
|
if storage_info := self.get_storage_info(storage_id):
|
||||||
# 夸克根文件夹
|
if storage_info["driver"] == "Quark":
|
||||||
quark_root_dir = self.get_root_folder_full_path(
|
addition = json.loads(storage_info["addition"])
|
||||||
addition["cookie"], addition["root_folder_id"]
|
# 存储挂载路径
|
||||||
)
|
storage_mount_path = storage_info["mount_path"]
|
||||||
if storage_mount_path and quark_root_dir:
|
# 夸克根文件夹
|
||||||
return True, (storage_mount_path, quark_root_dir)
|
quark_root_dir = self.get_root_folder_full_path(
|
||||||
else:
|
addition["cookie"], addition["root_folder_id"]
|
||||||
print(f"Alist刷新: 不支持[{storage_info['driver']}]驱动 ❌")
|
)
|
||||||
|
elif storage_info["driver"] == "QuarkTV":
|
||||||
|
print(
|
||||||
|
f"Alist刷新: [QuarkTV]驱动⚠️ storage_id请手动填入 /Alist挂载路径:/Quark目录路径"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"Alist刷新: 不支持[{storage_info['driver']}]驱动 ❌")
|
||||||
|
else:
|
||||||
|
print(f"Alist刷新: storage_id[{storage_id}]格式错误❌")
|
||||||
|
# 返回结果
|
||||||
|
if storage_mount_path and quark_root_dir:
|
||||||
|
return True, (storage_mount_path, quark_root_dir)
|
||||||
|
else:
|
||||||
|
return False, (None, None)
|
||||||
|
|
||||||
def get_storage_info(self, storage_id):
|
def get_storage_info(self, storage_id):
|
||||||
url = f"{self.url}/api/admin/storage/get"
|
url = f"{self.url}/api/admin/storage/get"
|
||||||
@ -94,11 +108,29 @@ class Alist:
|
|||||||
return data.get("data", [])
|
return data.get("data", [])
|
||||||
else:
|
else:
|
||||||
print(f"Alist刷新: 存储{storage_id}连接失败❌ {data.get('message')}")
|
print(f"Alist刷新: 存储{storage_id}连接失败❌ {data.get('message')}")
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"Alist刷新: 获取Alist存储出错 {e}")
|
print(f"Alist刷新: 获取Alist存储出错 {e}")
|
||||||
return False
|
return []
|
||||||
|
|
||||||
def refresh(self, path, force_refresh=True):
|
def refresh(self, path):
|
||||||
|
data = self.get_file_list(path, True)
|
||||||
|
if data.get("code") == 200:
|
||||||
|
print(f"📁 Alist刷新:目录[{path}] 成功✅")
|
||||||
|
return data.get("data")
|
||||||
|
elif "object not found" in data.get("message", ""):
|
||||||
|
# 如果是根目录就不再往上查找
|
||||||
|
if path == "/" or path == self.storage_mount_path:
|
||||||
|
print(f"📁 Alist刷新:根目录不存在,请检查 Alist 配置")
|
||||||
|
return False
|
||||||
|
# 获取父目录
|
||||||
|
parent_path = os.path.dirname(path)
|
||||||
|
print(f"📁 Alist刷新:[{path}] 不存在,转父目录 [{parent_path}]")
|
||||||
|
# 递归刷新父目录
|
||||||
|
return self.refresh(parent_path)
|
||||||
|
else:
|
||||||
|
print(f"📁 Alist刷新:失败❌ {data.get('message')}")
|
||||||
|
|
||||||
|
def get_file_list(self, path, force_refresh=False):
|
||||||
url = f"{self.url}/api/fs/list"
|
url = f"{self.url}/api/fs/list"
|
||||||
headers = {"Authorization": self.token}
|
headers = {"Authorization": self.token}
|
||||||
payload = {
|
payload = {
|
||||||
@ -111,25 +143,10 @@ class Alist:
|
|||||||
try:
|
try:
|
||||||
response = requests.request("POST", url, headers=headers, json=payload)
|
response = requests.request("POST", url, headers=headers, json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
return response.json()
|
||||||
if data.get("code") == 200:
|
except Exception as e:
|
||||||
print(f"📁 Alist刷新:目录[{path}] 成功✅")
|
print(f"📁 Alist刷新: 获取文件列表出错❌ {e}")
|
||||||
return data.get("data")
|
return {}
|
||||||
elif "object not found" in data.get("message", ""):
|
|
||||||
# 如果是根目录就不再往上查找
|
|
||||||
if path == "/" or path == self.storage_mount_path:
|
|
||||||
print(f"📁 Alist刷新:根目录不存在,请检查 Alist 配置")
|
|
||||||
return False
|
|
||||||
# 获取父目录
|
|
||||||
parent_path = os.path.dirname(path)
|
|
||||||
print(f"📁 Alist刷新:[{path}] 不存在,转父目录 [{parent_path}]")
|
|
||||||
# 递归刷新父目录
|
|
||||||
return self.refresh(parent_path)
|
|
||||||
else:
|
|
||||||
print(f"📁 Alist刷新:失败❌ {data.get('message')}")
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"Alist刷新目录出错: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_root_folder_full_path(self, cookie, pdir_fid):
|
def get_root_folder_full_path(self, cookie, pdir_fid):
|
||||||
if pdir_fid == "0":
|
if pdir_fid == "0":
|
||||||
@ -156,10 +173,10 @@ class Alist:
|
|||||||
"GET", url, headers=headers, params=querystring
|
"GET", url, headers=headers, params=querystring
|
||||||
).json()
|
).json()
|
||||||
if response["code"] == 0:
|
if response["code"] == 0:
|
||||||
file_names = [
|
path = ""
|
||||||
item["file_name"] for item in response["data"]["full_path"]
|
for item in response["data"]["full_path"]:
|
||||||
]
|
path = f"{path}/{item['file_name']}"
|
||||||
return "/".join(file_names)
|
return path
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"Alist刷新: 获取Quark路径出错 {e}")
|
print(f"Alist刷新: 获取Quark路径出错 {e}")
|
||||||
return False
|
return ""
|
||||||
@ -12,7 +12,7 @@ class Alist_strm:
|
|||||||
default_config = {
|
default_config = {
|
||||||
"url": "", # alist-strm服务器URL
|
"url": "", # alist-strm服务器URL
|
||||||
"cookie": "", # alist-strm的cookie,F12抓取,关键参数:session=ey***
|
"cookie": "", # alist-strm的cookie,F12抓取,关键参数:session=ey***
|
||||||
"config_id": "", # 要触发运行的配置ID
|
"config_id": "", # 要触发运行的配置ID,支持多个,用逗号分隔
|
||||||
}
|
}
|
||||||
is_active = False
|
is_active = False
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ class Alist_strm:
|
|||||||
if self.get_info(self.config_id):
|
if self.get_info(self.config_id):
|
||||||
self.is_active = True
|
self.is_active = True
|
||||||
|
|
||||||
def run(self, task):
|
def run(self, task, **kwargs):
|
||||||
self.run_selected_configs(self.config_id)
|
self.run_selected_configs(self.config_id)
|
||||||
|
|
||||||
def get_info(self, config_id_str):
|
def get_info(self, config_id_str):
|
||||||
@ -50,8 +50,8 @@ class Alist_strm:
|
|||||||
print(f"alist-strm配置运行: {config_name}")
|
print(f"alist-strm配置运行: {config_name}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"alist-strm配置运行: 匹配失败❌")
|
print(f"alist-strm配置运行: 匹配失败❌,请检查网络连通和cookie有效性")
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"获取alist-strm配置信息出错: {e}")
|
print(f"获取alist-strm配置信息出错: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -61,8 +61,8 @@ class Alist_strm:
|
|||||||
try:
|
try:
|
||||||
selected_configs = [int(x.strip()) for x in selected_configs_str.split(",")]
|
selected_configs = [int(x.strip()) for x in selected_configs_str.split(",")]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("Error: 运行alist-strm配置错误,id应以,分割")
|
print("🔗 alist-strm配置运行: 出错❌ id应以,分割")
|
||||||
return None
|
return False
|
||||||
data = [("selected_configs", config_id) for config_id in selected_configs]
|
data = [("selected_configs", config_id) for config_id in selected_configs]
|
||||||
data.append(("action", "run_selected"))
|
data.append(("action", "run_selected"))
|
||||||
try:
|
try:
|
||||||
@ -77,6 +77,6 @@ class Alist_strm:
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"🔗 alist-strm配置运行: 失败❌")
|
print(f"🔗 alist-strm配置运行: 失败❌")
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return False
|
return False
|
||||||
@ -24,6 +24,9 @@ class Alist_strm_gen:
|
|||||||
"strm_save_dir": "/media", # 生成的 strm 文件保存的路径
|
"strm_save_dir": "/media", # 生成的 strm 文件保存的路径
|
||||||
"strm_replace_host": "", # strm 文件内链接的主机地址 (可选,缺省时=url)
|
"strm_replace_host": "", # strm 文件内链接的主机地址 (可选,缺省时=url)
|
||||||
}
|
}
|
||||||
|
default_task_config = {
|
||||||
|
"auto_gen": True, # 是否自动生成 strm 文件
|
||||||
|
}
|
||||||
is_active = False
|
is_active = False
|
||||||
# 缓存参数
|
# 缓存参数
|
||||||
storage_mount_path = None
|
storage_mount_path = None
|
||||||
@ -31,12 +34,13 @@ class Alist_strm_gen:
|
|||||||
strm_server = None
|
strm_server = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self.plugin_name = self.__class__.__name__.lower()
|
||||||
if kwargs:
|
if kwargs:
|
||||||
for key, _ in self.default_config.items():
|
for key, _ in self.default_config.items():
|
||||||
if key in kwargs:
|
if key in kwargs:
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
else:
|
else:
|
||||||
print(f"{self.__class__.__name__} 模块缺少必要参数: {key}")
|
print(f"{self.plugin_name} 模块缺少必要参数: {key}")
|
||||||
if self.url and self.token and self.storage_id:
|
if self.url and self.token and self.storage_id:
|
||||||
success, result = self.storage_id_to_path(self.storage_id)
|
success, result = self.storage_id_to_path(self.storage_id)
|
||||||
if success:
|
if success:
|
||||||
@ -53,7 +57,12 @@ class Alist_strm_gen:
|
|||||||
else:
|
else:
|
||||||
self.strm_server = f"{self.url.strip()}/d"
|
self.strm_server = f"{self.url.strip()}/d"
|
||||||
|
|
||||||
def run(self, task):
|
def run(self, task, **kwargs):
|
||||||
|
task_config = task.get("addition", {}).get(
|
||||||
|
self.plugin_name, self.default_task_config
|
||||||
|
)
|
||||||
|
if not task_config.get("auto_gen"):
|
||||||
|
return
|
||||||
if task.get("savepath") and task.get("savepath").startswith(
|
if task.get("savepath") and task.get("savepath").startswith(
|
||||||
self.quark_root_dir
|
self.quark_root_dir
|
||||||
):
|
):
|
||||||
@ -63,28 +72,43 @@ class Alist_strm_gen:
|
|||||||
task["savepath"].replace(self.quark_root_dir, "", 1).lstrip("/"),
|
task["savepath"].replace(self.quark_root_dir, "", 1).lstrip("/"),
|
||||||
)
|
)
|
||||||
).replace("\\", "/")
|
).replace("\\", "/")
|
||||||
self.refresh(alist_path)
|
self.check_dir(alist_path)
|
||||||
|
|
||||||
def storage_id_to_path(self, storage_id):
|
def storage_id_to_path(self, storage_id):
|
||||||
|
storage_mount_path, quark_root_dir = None, None
|
||||||
# 1. 检查是否符合 /aaa:/bbb 格式
|
# 1. 检查是否符合 /aaa:/bbb 格式
|
||||||
match = re.match(r"^(\/[^:]*):(\/[^:]*)$", storage_id)
|
if match := re.match(r"^(\/[^:]*):(\/[^:]*)$", storage_id):
|
||||||
if match:
|
# 存储挂载路径, 夸克根文件夹
|
||||||
return True, (match.group(1), match.group(2))
|
storage_mount_path, quark_root_dir = match.group(1), match.group(2)
|
||||||
# 2. 调用 Alist API 获取存储信息
|
file_list = self.get_file_list(storage_mount_path)
|
||||||
storage_info = self.get_storage_info(storage_id)
|
if file_list.get("code") != 200:
|
||||||
if storage_info:
|
print(f"Alist-Strm生成: 获取挂载路径失败❌ {file_list.get('message')}")
|
||||||
if storage_info["driver"] == "Quark":
|
return False, (None, None)
|
||||||
addition = json.loads(storage_info["addition"])
|
# 2. 检查是否数字,调用 Alist API 获取存储信息
|
||||||
# 存储挂载路径
|
elif re.match(r"^\d+$", storage_id):
|
||||||
storage_mount_path = storage_info["mount_path"]
|
if storage_info := self.get_storage_info(storage_id):
|
||||||
# 夸克根文件夹
|
if storage_info["driver"] == "Quark":
|
||||||
quark_root_dir = self.get_root_folder_full_path(
|
addition = json.loads(storage_info["addition"])
|
||||||
addition["cookie"], addition["root_folder_id"]
|
# 存储挂载路径
|
||||||
)
|
storage_mount_path = storage_info["mount_path"]
|
||||||
if storage_mount_path and quark_root_dir:
|
# 夸克根文件夹
|
||||||
return True, (storage_mount_path, quark_root_dir)
|
quark_root_dir = self.get_root_folder_full_path(
|
||||||
else:
|
addition["cookie"], addition["root_folder_id"]
|
||||||
print(f"Alist刷新: 不支持[{storage_info['driver']}]驱动 ❌")
|
)
|
||||||
|
elif storage_info["driver"] == "QuarkTV":
|
||||||
|
print(
|
||||||
|
f"Alist-Strm生成: [QuarkTV]驱动⚠️ storage_id请手动填入 /Alist挂载路径:/Quark目录路径"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"Alist-Strm生成: 不支持[{storage_info['driver']}]驱动 ❌")
|
||||||
|
else:
|
||||||
|
print(f"Alist-Strm生成: storage_id[{storage_id}]格式错误❌")
|
||||||
|
# 返回结果
|
||||||
|
if storage_mount_path and quark_root_dir:
|
||||||
|
print(f"Alist-Strm生成: [{storage_mount_path}:{quark_root_dir}]")
|
||||||
|
return True, (storage_mount_path, quark_root_dir)
|
||||||
|
else:
|
||||||
|
return False, (None, None)
|
||||||
|
|
||||||
def get_storage_info(self, storage_id):
|
def get_storage_info(self, storage_id):
|
||||||
url = f"{self.url}/api/admin/storage/get"
|
url = f"{self.url}/api/admin/storage/get"
|
||||||
@ -95,48 +119,45 @@ class Alist_strm_gen:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if data.get("code") == 200:
|
if data.get("code") == 200:
|
||||||
print(
|
|
||||||
f"Alist-Strm生成: {data['data']['driver']}[{data['data']['mount_path']}]"
|
|
||||||
)
|
|
||||||
return data.get("data", [])
|
return data.get("data", [])
|
||||||
else:
|
else:
|
||||||
print(f"Alist-Strm生成: 连接失败❌ {response.get('message')}")
|
print(f"Alist-Strm生成: 获取存储失败❌ {data.get('message')}")
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"Alist-Strm生成: 获取Alist存储出错 {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def refresh(self, path):
|
|
||||||
try:
|
|
||||||
response = self.get_file_list(path)
|
|
||||||
if response.get("code") != 200:
|
|
||||||
print(f"📺 生成 STRM 文件失败❌ {response.get('message')}")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
files = response.get("data").get("content")
|
|
||||||
for item in files:
|
|
||||||
item_path = f"{path}/{item.get('name')}".replace("//", "/")
|
|
||||||
if item.get("is_dir"):
|
|
||||||
self.refresh(item_path)
|
|
||||||
else:
|
|
||||||
self.generate_strm(item_path)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"📺 获取 Alist 文件列表失败❌ {e}")
|
print(f"Alist-Strm生成: 获取存储出错 {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def get_file_list(self, path):
|
def check_dir(self, path):
|
||||||
|
data = self.get_file_list(path)
|
||||||
|
if data.get("code") != 200:
|
||||||
|
print(f"📺 Alist-Strm生成: 获取文件列表失败❌{data.get('message')}")
|
||||||
|
return
|
||||||
|
elif files := data.get("data", {}).get("content"):
|
||||||
|
for item in files:
|
||||||
|
item_path = f"{path}/{item.get('name')}".replace("//", "/")
|
||||||
|
if item.get("is_dir"):
|
||||||
|
self.check_dir(item_path)
|
||||||
|
else:
|
||||||
|
self.generate_strm(item_path, item)
|
||||||
|
|
||||||
|
def get_file_list(self, path, force_refresh=False):
|
||||||
url = f"{self.url}/api/fs/list"
|
url = f"{self.url}/api/fs/list"
|
||||||
headers = {"Authorization": self.token}
|
headers = {"Authorization": self.token}
|
||||||
payload = {
|
payload = {
|
||||||
"path": path,
|
"path": path,
|
||||||
"refresh": False,
|
"refresh": force_refresh,
|
||||||
"password": "",
|
"password": "",
|
||||||
"page": 1,
|
"page": 1,
|
||||||
"per_page": 0,
|
"per_page": 0,
|
||||||
}
|
}
|
||||||
response = requests.request("POST", url, headers=headers, json=payload)
|
try:
|
||||||
response.raise_for_status()
|
response = requests.request("POST", url, headers=headers, json=payload)
|
||||||
return response.json()
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"📺 Alist-Strm生成: 获取文件列表出错❌ {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
def generate_strm(self, file_path):
|
def generate_strm(self, file_path, file_info):
|
||||||
ext = file_path.split(".")[-1]
|
ext = file_path.split(".")[-1]
|
||||||
if ext.lower() in self.video_exts:
|
if ext.lower() in self.video_exts:
|
||||||
strm_path = (
|
strm_path = (
|
||||||
@ -148,8 +169,11 @@ class Alist_strm_gen:
|
|||||||
return
|
return
|
||||||
if not os.path.exists(os.path.dirname(strm_path)):
|
if not os.path.exists(os.path.dirname(strm_path)):
|
||||||
os.makedirs(os.path.dirname(strm_path))
|
os.makedirs(os.path.dirname(strm_path))
|
||||||
|
sign_param = (
|
||||||
|
"" if not file_info.get("sign") else f"?sign={file_info['sign']}"
|
||||||
|
)
|
||||||
with open(strm_path, "w", encoding="utf-8") as strm_file:
|
with open(strm_path, "w", encoding="utf-8") as strm_file:
|
||||||
strm_file.write(f"{self.strm_server}{file_path}")
|
strm_file.write(f"{self.strm_server}{file_path}{sign_param}")
|
||||||
print(f"📺 生成STRM文件 {strm_path} 成功✅")
|
print(f"📺 生成STRM文件 {strm_path} 成功✅")
|
||||||
|
|
||||||
def get_root_folder_full_path(self, cookie, pdir_fid):
|
def get_root_folder_full_path(self, cookie, pdir_fid):
|
||||||
@ -177,10 +201,10 @@ class Alist_strm_gen:
|
|||||||
"GET", url, headers=headers, params=querystring
|
"GET", url, headers=headers, params=querystring
|
||||||
).json()
|
).json()
|
||||||
if response["code"] == 0:
|
if response["code"] == 0:
|
||||||
file_names = [
|
path = ""
|
||||||
item["file_name"] for item in response["data"]["full_path"]
|
for item in response["data"]["full_path"]:
|
||||||
]
|
path = f"{path}/{item['file_name']}"
|
||||||
return "/".join(file_names)
|
return path
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"Alist-Strm生成: 获取Quark路径出错 {e}")
|
print(f"Alist-Strm生成: 获取Quark路径出错 {e}")
|
||||||
return False
|
return ""
|
||||||
313
plugins/alist_sync.py
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
# plugins: 调用 alist 实现跨网盘转存
|
||||||
|
# author: https://github.com/jenfonro
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class Alist_sync:
|
||||||
|
|
||||||
|
default_config = {
|
||||||
|
"url": "", # Alist服务器URL
|
||||||
|
"token": "", # Alist服务器Token
|
||||||
|
"quark_storage_id": "", # Alist 服务器夸克存储 ID
|
||||||
|
"save_storage_id": "", # Alist 服务器同步的存储 ID
|
||||||
|
"tv_mode": "", # TV库模式,填入非0值开启
|
||||||
|
# TV库模式说明:
|
||||||
|
# 1.开启后,会验证文件名是否包含S01E01的正则,格式目前仅包含mp4及mkv,
|
||||||
|
# 2.会对比保存目录下是否存在该名称的mp4、mkv文件,如果不存在才会进行同步
|
||||||
|
# 3.夸克目录及同步目录均会提取为S01E01的正则进行匹配,不受其它字符影响
|
||||||
|
}
|
||||||
|
is_active = False
|
||||||
|
# 缓存参数
|
||||||
|
|
||||||
|
default_task_config = {
|
||||||
|
"enable": False, # 当前任务开关,
|
||||||
|
"save_path": "", # 需要同步目录,默认空时路径则会与夸克的保存路径一致,不开启完整路径模式时,默认根目录为保存驱动的根目录
|
||||||
|
"verify_path": "", # 验证目录,主要用于影视库避免重复文件,一般配合alist的别名功能及full_path_mode使用,用于多个网盘的源合并成一个目录
|
||||||
|
"full_path_mode": False, # 完整路径模式
|
||||||
|
# 完整路径模式开启后不再限制保存目录的存储驱动,将根据填入的路径进行保存,需要填写完整的alist目录
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
if kwargs:
|
||||||
|
for key, _ in self.default_config.items():
|
||||||
|
if key in kwargs:
|
||||||
|
setattr(self, key, kwargs[key])
|
||||||
|
else:
|
||||||
|
print(f"{self.__class__.__name__} 模块缺少必要参数: {key}")
|
||||||
|
if self.url and self.token:
|
||||||
|
if self.verify_server():
|
||||||
|
self.is_active = True
|
||||||
|
|
||||||
|
def _send_request(self, method, url, **kwargs):
|
||||||
|
headers = {"Authorization": self.token, "Content-Type": "application/json"}
|
||||||
|
if "headers" in kwargs:
|
||||||
|
headers = kwargs["headers"]
|
||||||
|
del kwargs["headers"]
|
||||||
|
try:
|
||||||
|
response = requests.request(method, url, headers=headers, **kwargs)
|
||||||
|
# print(f"{response.text}")
|
||||||
|
# response.raise_for_status() # 检查请求是否成功,但返回非200也会抛出异常
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
print(f"_send_request error:\n{e}")
|
||||||
|
fake_response = requests.Response()
|
||||||
|
fake_response.status_code = 500
|
||||||
|
fake_response._content = b'{"status": 500, "message": "request error"}'
|
||||||
|
return fake_response
|
||||||
|
|
||||||
|
def verify_server(self):
|
||||||
|
url = f"{self.url}/api/me"
|
||||||
|
querystring = ""
|
||||||
|
headers = {"Authorization": self.token, "Content-Type": "application/json"}
|
||||||
|
try:
|
||||||
|
response = requests.request("GET", url, headers=headers, params=querystring)
|
||||||
|
response.raise_for_status()
|
||||||
|
response = response.json()
|
||||||
|
if response.get("code") == 200:
|
||||||
|
if response.get("data").get("username") == "guest":
|
||||||
|
print(f"Alist登陆失败,请检查token")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Alist登陆成功,当前用户: {response.get('data').get('username')}"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Alist同步: 连接服务器失败❌ {response.get('message')}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"获取Alist信息出错: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run(self, task, **kwargs):
|
||||||
|
if not task["addition"]["alist_sync"]["enable"]:
|
||||||
|
return 0
|
||||||
|
print(f"开始进行alist同步")
|
||||||
|
# 这一块注释的是获取任务的参数,在web界面可以看
|
||||||
|
# print("所有任务参数:")
|
||||||
|
# print(task)
|
||||||
|
# print(task['addition'])
|
||||||
|
# print(task['addition']['alist_sync'])
|
||||||
|
# print(task['addition']['alist_sync']['target_path'])
|
||||||
|
|
||||||
|
# 获取夸克挂载根目录
|
||||||
|
data = self.get_storage_path(self.quark_storage_id)
|
||||||
|
if data["driver"] != "Quark":
|
||||||
|
print(
|
||||||
|
f"Alist同步: 存储{self.quark_storage_id}非夸克存储❌ {data['driver']}"
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
quark_mount_root_path = re.sub(r".*root_folder_id\":\"", "", data["addition"])
|
||||||
|
quark_mount_root_path = re.sub(r"\",.*", "", quark_mount_root_path)
|
||||||
|
if quark_mount_root_path != "0" and quark_mount_root_path != "":
|
||||||
|
print(f"Alist同步: 存储{self.quark_storage_id}挂载的目录非夸克根目录❌")
|
||||||
|
return 0
|
||||||
|
self.quark_mount_path = data["mount_path"]
|
||||||
|
|
||||||
|
# 获取保存路径的挂载根目录
|
||||||
|
if self.save_storage_id != 0:
|
||||||
|
data = self.get_storage_path(self.save_storage_id)
|
||||||
|
self.save_mount_path = data["mount_path"]
|
||||||
|
|
||||||
|
# 保存的目录初始化
|
||||||
|
if task["addition"]["alist_sync"]["save_path"] == "":
|
||||||
|
self.save_path = f"{self.save_mount_path}/{task['savepath']}"
|
||||||
|
else:
|
||||||
|
self.save_path = task["addition"]["alist_sync"]["save_path"]
|
||||||
|
if not task["addition"]["alist_sync"]["full_path_mode"]:
|
||||||
|
if self.save_path.startswith("/"):
|
||||||
|
self.save_path = self.save_path[1:]
|
||||||
|
if self.save_path.endswith("/"):
|
||||||
|
self.save_path = self.save_path[:-1]
|
||||||
|
self.save_path = f"{self.save_mount_path}/{self.save_path}"
|
||||||
|
else:
|
||||||
|
# print('完整路径模式')
|
||||||
|
if not self.save_path.startswith("/"):
|
||||||
|
self.save_path = "/" + self.save_path
|
||||||
|
if self.save_path.endswith("/"):
|
||||||
|
self.save_path = self.save_path[:-1]
|
||||||
|
|
||||||
|
# 获取保存目录是否存在
|
||||||
|
if not self.get_path(self.save_path):
|
||||||
|
dir_exists = False
|
||||||
|
# 如果目录不存在判断两边路径是否一致,一致时直接创建复制目录任务即可
|
||||||
|
|
||||||
|
else:
|
||||||
|
dir_exists = True
|
||||||
|
copy_dir = False
|
||||||
|
|
||||||
|
# 初始化验证目录
|
||||||
|
# 如果没有填验证目录,则验证目录与保存目录一致
|
||||||
|
|
||||||
|
if task["addition"]["alist_sync"]["verify_path"]:
|
||||||
|
self.verify_path = task["addition"]["alist_sync"]["verify_path"]
|
||||||
|
if not task["addition"]["alist_sync"]["full_path_mode"]:
|
||||||
|
if self.verify_path.startswith("/"):
|
||||||
|
self.verify_path = self.save_path[1:]
|
||||||
|
if self.verify_path.endswith("/"):
|
||||||
|
self.verify_path = self.save_path[:-1]
|
||||||
|
self.verify_path = f"{self.save_mount_path}/{self.verify_path}"
|
||||||
|
else:
|
||||||
|
# print('完整路径模式')
|
||||||
|
if not self.verify_path.startswith("/"):
|
||||||
|
self.verify_path = "/" + self.save_path
|
||||||
|
if self.verify_path.endswith("/"):
|
||||||
|
self.verify_path = self.save_path[:-1]
|
||||||
|
else:
|
||||||
|
self.verify_path = self.save_path
|
||||||
|
|
||||||
|
# 初始化夸克目录
|
||||||
|
self.source_path = f"{self.quark_mount_path}/{task['savepath']}"
|
||||||
|
# 初始化任务名
|
||||||
|
self.taskname = f"{task['taskname']}"
|
||||||
|
|
||||||
|
# 获取网盘已有文件
|
||||||
|
source_dir_list = self.get_path_list(self.source_path)
|
||||||
|
if not source_dir_list:
|
||||||
|
print("获取夸克文件列表失败,请检查网络或手动刷新alist中的夸克目录")
|
||||||
|
return 0
|
||||||
|
if self.tv_mode == 0 or self.tv_mode == "":
|
||||||
|
self.tv_mode = False
|
||||||
|
else:
|
||||||
|
self.tv_mode = True
|
||||||
|
|
||||||
|
# 如果是新建的目录则将所有文件直接复制
|
||||||
|
if not dir_exists:
|
||||||
|
self.get_save_file([], source_dir_list)
|
||||||
|
else:
|
||||||
|
verify_dir_list = self.get_path_list(self.verify_path)
|
||||||
|
if verify_dir_list:
|
||||||
|
self.get_save_file(verify_dir_list, source_dir_list)
|
||||||
|
else:
|
||||||
|
self.get_save_file([], source_dir_list)
|
||||||
|
|
||||||
|
if self.save_file_data:
|
||||||
|
self.save_start(self.save_file_data)
|
||||||
|
print("同步的文件列表:")
|
||||||
|
for save_file in self.save_file_data:
|
||||||
|
print(f"└── 🎞️{save_file}")
|
||||||
|
else:
|
||||||
|
print("没有需要同步的文件")
|
||||||
|
|
||||||
|
def save_start(self, save_file_data):
|
||||||
|
url = f"{self.url}/api/fs/copy"
|
||||||
|
payload = json.dumps(
|
||||||
|
{
|
||||||
|
"src_dir": self.source_path,
|
||||||
|
"dst_dir": self.save_path,
|
||||||
|
"names": save_file_data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = self._send_request("POST", url, data=payload)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print("未能进行Alist同步,请手动同步")
|
||||||
|
else:
|
||||||
|
print("Alist创建任务成功")
|
||||||
|
self.copy_task = response.json()
|
||||||
|
|
||||||
|
def get_save_file(self, target_dir_list, source_dir_list):
|
||||||
|
self.save_file_data = []
|
||||||
|
if target_dir_list == []:
|
||||||
|
for source_list in source_dir_list:
|
||||||
|
if self.tv_mode:
|
||||||
|
if re.search(
|
||||||
|
self.taskname + r"\.s\d{1,3}e\d{1,3}\.(mkv|mp4)",
|
||||||
|
source_list["name"],
|
||||||
|
re.IGNORECASE,
|
||||||
|
):
|
||||||
|
self.save_file_data.append(source_list["name"])
|
||||||
|
else:
|
||||||
|
self.save_file_data.append(source_list["name"])
|
||||||
|
else:
|
||||||
|
for source_list in source_dir_list:
|
||||||
|
skip = False
|
||||||
|
source_list_filename = (
|
||||||
|
source_list["name"]
|
||||||
|
.replace(".mp4", "")
|
||||||
|
.replace(".mkv", "")
|
||||||
|
.replace(self.taskname + ".", "")
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
for target_list in target_dir_list:
|
||||||
|
if source_list["is_dir"]:
|
||||||
|
# print(f"跳过目录同步")
|
||||||
|
skip = True
|
||||||
|
break
|
||||||
|
if self.tv_mode:
|
||||||
|
target_list_filename = (
|
||||||
|
target_list["name"]
|
||||||
|
.replace(".mp4", "")
|
||||||
|
.replace(".mkv", "")
|
||||||
|
.replace(self.taskname + ".", "")
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
if source_list_filename == target_list_filename:
|
||||||
|
# print(f"文件存在,名称为:{target_list['name']}")
|
||||||
|
skip = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if source_list["name"] == target_list["name"]:
|
||||||
|
# print(f"文件存在,名称为:{target_dir['name']}")
|
||||||
|
skip = True
|
||||||
|
break
|
||||||
|
if self.tv_mode:
|
||||||
|
if re.search(
|
||||||
|
self.taskname + r"\.s\d{1,3}e\d{1,3}\.(mkv|mp4)",
|
||||||
|
source_list["name"],
|
||||||
|
re.IGNORECASE,
|
||||||
|
):
|
||||||
|
# 添加一句验证,如果有MKV,MP4存在时,则只保存某一个格式
|
||||||
|
if re.search(
|
||||||
|
self.taskname + r"\.s\d{1,3}e\d{1,3}\.mp4",
|
||||||
|
source_list["name"],
|
||||||
|
re.IGNORECASE,
|
||||||
|
):
|
||||||
|
for all_file in source_dir_list:
|
||||||
|
if (
|
||||||
|
source_list["name"].replace(".mp4", ".mkv")
|
||||||
|
== all_file["name"]
|
||||||
|
):
|
||||||
|
print(
|
||||||
|
f"{source_list['name']}拥有相同版本的MKV文件,跳过复制"
|
||||||
|
)
|
||||||
|
skip = True
|
||||||
|
if not skip:
|
||||||
|
self.save_file_data.append(source_list["name"])
|
||||||
|
|
||||||
|
def get_path_list(self, path):
|
||||||
|
url = f"{self.url}/api/fs/list"
|
||||||
|
payload = json.dumps(
|
||||||
|
{"path": path, "password": "", "page": 1, "per_page": 0, "refresh": True}
|
||||||
|
)
|
||||||
|
response = self._send_request("POST", url, data=payload)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"获取Alist目录出错: {response}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return response.json()["data"]["content"]
|
||||||
|
|
||||||
|
def get_path(self, path):
|
||||||
|
url = f"{self.url}/api/fs/list"
|
||||||
|
payload = json.dumps({"path": path, "password": "", "force_root": False})
|
||||||
|
response = self._send_request("POST", url, data=payload)
|
||||||
|
if response.status_code != 200 or response.json()["message"] != "success":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_storage_path(self, storage_id):
|
||||||
|
url = f"{self.url}/api/admin/storage/get"
|
||||||
|
headers = {"Authorization": self.token}
|
||||||
|
querystring = {"id": storage_id}
|
||||||
|
try:
|
||||||
|
response = requests.request("GET", url, headers=headers, params=querystring)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
if data.get("code") == 200:
|
||||||
|
return data.get("data", [])
|
||||||
|
else:
|
||||||
|
print(f"Alist同步: 存储{storage_id}连接失败❌ {data.get('message')}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Alist同步: 获取Alist存储出错 {e}")
|
||||||
|
return []
|
||||||
103
plugins/aria2.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class Aria2:
|
||||||
|
|
||||||
|
default_config = {
|
||||||
|
"host_port": "172.17.0.1:6800", # Aria2 RPC地址
|
||||||
|
"secret": "", # Aria2 RPC 密钥
|
||||||
|
"dir": "/Downloads", # 下载目录,需要Aria2有权限访问
|
||||||
|
}
|
||||||
|
default_task_config = {
|
||||||
|
"auto_download": False, # 是否自动添加下载任务
|
||||||
|
"pause": False, # 添加任务后为暂停状态,不自动开始(手动下载)
|
||||||
|
}
|
||||||
|
is_active = False
|
||||||
|
rpc_url = None
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.plugin_name = self.__class__.__name__.lower()
|
||||||
|
if kwargs:
|
||||||
|
for key, _ in self.default_config.items():
|
||||||
|
if key in kwargs:
|
||||||
|
setattr(self, key, kwargs[key])
|
||||||
|
else:
|
||||||
|
print(f"{self.plugin_name} 模块缺少必要参数: {key}")
|
||||||
|
if self.host_port and self.secret:
|
||||||
|
self.rpc_url = f"http://{self.host_port}/jsonrpc"
|
||||||
|
if self.get_version():
|
||||||
|
self.is_active = True
|
||||||
|
|
||||||
|
def run(self, task, **kwargs):
|
||||||
|
task_config = task.get("addition", {}).get(
|
||||||
|
self.plugin_name, self.default_task_config
|
||||||
|
)
|
||||||
|
if not task_config.get("auto_download"):
|
||||||
|
return
|
||||||
|
if (tree := kwargs.get("tree")) and (account := kwargs.get("account")):
|
||||||
|
# 按文件路径排序添加下载任务
|
||||||
|
nodes = sorted(
|
||||||
|
tree.all_nodes_itr(), key=lambda node: node.data.get("path", "")
|
||||||
|
)
|
||||||
|
file_fids = []
|
||||||
|
file_paths = []
|
||||||
|
for node in nodes:
|
||||||
|
if not node.data.get("is_dir", True):
|
||||||
|
file_fids.append(node.data.get("fid"))
|
||||||
|
file_paths.append(node.data.get("path"))
|
||||||
|
if not file_fids:
|
||||||
|
print(f"Aria2下载: 没有下载任务,跳过")
|
||||||
|
return
|
||||||
|
download_return, cookie = account.download(file_fids)
|
||||||
|
file_urls = [item["download_url"] for item in download_return["data"]]
|
||||||
|
for index, file_url in enumerate(file_urls):
|
||||||
|
file_path = file_paths[index]
|
||||||
|
print(f"📥 Aria2下载: {file_path}")
|
||||||
|
local_path = f"{self.dir}{file_paths[index]}"
|
||||||
|
aria2_params = [
|
||||||
|
[file_url],
|
||||||
|
{
|
||||||
|
"header": [
|
||||||
|
f"Cookie: {cookie or account.cookie}",
|
||||||
|
f"User-Agent: {account.USER_AGENT}",
|
||||||
|
],
|
||||||
|
"out": os.path.basename(local_path),
|
||||||
|
"dir": os.path.dirname(local_path),
|
||||||
|
"pause": task_config.get("pause"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.add_uri(aria2_params)
|
||||||
|
|
||||||
|
def _make_rpc_request(self, method, params=None):
|
||||||
|
"""发出 JSON-RPC 请求."""
|
||||||
|
jsonrpc_data = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "quark-auto-save",
|
||||||
|
"method": method,
|
||||||
|
"params": params or [],
|
||||||
|
}
|
||||||
|
if self.secret:
|
||||||
|
jsonrpc_data["params"].insert(0, f"token:{self.secret}")
|
||||||
|
try:
|
||||||
|
response = requests.post(self.rpc_url, json=jsonrpc_data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Aria2下载: 错误{e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
"""检查与 Aria2 的连接."""
|
||||||
|
response = self._make_rpc_request("aria2.getVersion")
|
||||||
|
if response.get("result"):
|
||||||
|
print(f"Aria2下载: v{response['result']['version']}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Aria2下载: 连接失败{response.get('error')}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_uri(self, params=None):
|
||||||
|
"""添加 URI 下载任务."""
|
||||||
|
response = self._make_rpc_request("aria2.addUri", params)
|
||||||
|
return response.get("result") if response else {}
|
||||||
@ -3,30 +3,41 @@ import requests
|
|||||||
|
|
||||||
class Emby:
|
class Emby:
|
||||||
|
|
||||||
default_config = {"url": "", "token": ""}
|
default_config = {
|
||||||
|
"url": "", # Emby服务器地址
|
||||||
|
"token": "", # Emby服务器token
|
||||||
|
}
|
||||||
|
default_task_config = {
|
||||||
|
"try_match": True, # 是否尝试匹配
|
||||||
|
"media_id": "", # 媒体ID,当为0时不刷新
|
||||||
|
}
|
||||||
is_active = False
|
is_active = False
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self.plugin_name = self.__class__.__name__.lower()
|
||||||
if kwargs:
|
if kwargs:
|
||||||
for key, value in self.default_config.items():
|
for key, _ in self.default_config.items():
|
||||||
if key in kwargs:
|
if key in kwargs:
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
else:
|
else:
|
||||||
print(f"{self.__class__.__name__} 模块缺少必要参数: {key}")
|
print(f"{self.plugin_name} 模块缺少必要参数: {key}")
|
||||||
if self.url and self.token:
|
if self.url and self.token:
|
||||||
if self.get_info():
|
if self.get_info():
|
||||||
self.is_active = True
|
self.is_active = True
|
||||||
|
|
||||||
def run(self, task):
|
def run(self, task, **kwargs):
|
||||||
if task.get("media_id"):
|
task_config = task.get("addition", {}).get(
|
||||||
if task["media_id"] != "0":
|
self.plugin_name, self.default_task_config
|
||||||
self.refresh(task["media_id"])
|
)
|
||||||
else:
|
if media_id := task_config.get("media_id"):
|
||||||
match_media_id = self.search(task["taskname"])
|
if media_id != "0":
|
||||||
if match_media_id:
|
self.refresh(media_id)
|
||||||
task["media_id"] = match_media_id
|
elif task_config.get("try_match"):
|
||||||
|
if match_media_id := self.search(task["taskname"]):
|
||||||
self.refresh(match_media_id)
|
self.refresh(match_media_id)
|
||||||
return task
|
task_config["media_id"] = match_media_id
|
||||||
|
task.setdefault("addition", {})[self.plugin_name] = task_config
|
||||||
|
return task
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
url = f"{self.url}/emby/System/Info"
|
url = f"{self.url}/emby/System/Info"
|
||||||
@ -42,7 +53,7 @@ class Emby:
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"Emby媒体库: 连接失败❌ {response.text}")
|
print(f"Emby媒体库: 连接失败❌ {response.text}")
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"获取Emby媒体库信息出错: {e}")
|
print(f"获取Emby媒体库信息出错: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -67,13 +78,13 @@ class Emby:
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"🎞️ 刷新Emby媒体库:{response.text}❌")
|
print(f"🎞️ 刷新Emby媒体库:{response.text}❌")
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"刷新Emby媒体库出错: {e}")
|
print(f"刷新Emby媒体库出错: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def search(self, media_name):
|
def search(self, media_name):
|
||||||
if not media_name:
|
if not media_name:
|
||||||
return False
|
return ""
|
||||||
url = f"{self.url}/emby/Items"
|
url = f"{self.url}/emby/Items"
|
||||||
headers = {"X-Emby-Token": self.token}
|
headers = {"X-Emby-Token": self.token}
|
||||||
querystring = {
|
querystring = {
|
||||||
@ -100,6 +111,6 @@ class Emby:
|
|||||||
return item["Id"]
|
return item["Id"]
|
||||||
else:
|
else:
|
||||||
print(f"🎞️ 搜索Emby媒体库:{response.text}❌")
|
print(f"🎞️ 搜索Emby媒体库:{response.text}❌")
|
||||||
except requests.exceptions.RequestException as e:
|
except Exception as e:
|
||||||
print(f"搜索Emby媒体库出错: {e}")
|
print(f"搜索Emby媒体库出错: {e}")
|
||||||
return False
|
return ""
|
||||||
312
plugins/fnv.py
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
# 飞牛影视插件
|
||||||
|
# 该插件用于与飞牛影视服务器API交互,支持自动刷新媒体库
|
||||||
|
# 通过配置用户名、密码和密钥字符串进行认证,并提供媒体库扫描功能
|
||||||
|
class Fnv:
|
||||||
|
# --- 配置信息 ---
|
||||||
|
default_config = {
|
||||||
|
"base_url": "http://10.0.0.6:5666", # 飞牛影视服务器URL
|
||||||
|
"app_name": "trimemedia-web", # 飞牛影视应用名称
|
||||||
|
"username": "", # 飞牛影视用户名
|
||||||
|
"password": "", # 飞牛影视密码
|
||||||
|
"secret_string": "", # 飞牛影视密钥字符串
|
||||||
|
"api_key": "", # 飞牛影视API密钥
|
||||||
|
"token": None, # 飞牛影视认证Token (可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
default_task_config = {
|
||||||
|
"auto_refresh": False, # 是否自动刷新媒体库
|
||||||
|
"mdb_name": "", # 飞牛影视目标媒体库名称
|
||||||
|
"mdb_dir_list": "", # 飞牛影视目标媒体库文件夹路径列表,多个用逗号分隔
|
||||||
|
}
|
||||||
|
|
||||||
|
# 定义一个可选键的集合
|
||||||
|
OPTIONAL_KEYS = {"token"}
|
||||||
|
|
||||||
|
# --- API 端点常量 ---
|
||||||
|
API_LOGIN = "/v/api/v1/login" # 登录端点
|
||||||
|
API_MDB_LIST = "/v/api/v1/mdb/list" # 获取媒体库列表
|
||||||
|
API_MDB_SCAN = "/v/api/v1/mdb/scan/{}" # 刷新媒体库端点 ({}为媒体库ID)
|
||||||
|
API_TASK_STOP = "/v/api/v1/task/stop" # 停止任务端点
|
||||||
|
|
||||||
|
# --- 实例状态 ---
|
||||||
|
is_active = False
|
||||||
|
session = requests.Session()
|
||||||
|
token = None
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# Public Methods / Entry Points (公共方法/入口)
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""
|
||||||
|
初始化 Fnv 客户端。
|
||||||
|
"""
|
||||||
|
self.plugin_name = self.__class__.__name__.lower()
|
||||||
|
if kwargs:
|
||||||
|
for key, _ in self.default_config.items():
|
||||||
|
if key in kwargs:
|
||||||
|
setattr(self, key, kwargs[key])
|
||||||
|
# 检查配置并尝试登录,以确定插件是否激活
|
||||||
|
if self._check_config():
|
||||||
|
if self.token is None or self.token == "":
|
||||||
|
self._login()
|
||||||
|
self.is_active = self.token is not None or self.token != ""
|
||||||
|
if self.is_active:
|
||||||
|
print(f"{self.plugin_name}: 插件已激活 ✅")
|
||||||
|
else:
|
||||||
|
print(f"{self.plugin_name}: 插件未激活 ❌")
|
||||||
|
|
||||||
|
def run(self, task, **kwargs):
|
||||||
|
"""
|
||||||
|
插件运行主入口。
|
||||||
|
根据任务配置,执行媒体库刷新操作。
|
||||||
|
"""
|
||||||
|
if not self.is_active:
|
||||||
|
print(f"飞牛影视: 插件未激活,跳过任务。")
|
||||||
|
return
|
||||||
|
|
||||||
|
task_config = task.get("addition", {}).get(
|
||||||
|
self.plugin_name, self.default_task_config
|
||||||
|
)
|
||||||
|
if not task_config.get("auto_refresh"):
|
||||||
|
print("飞牛影视: 自动刷新未启用,跳过处理。")
|
||||||
|
return
|
||||||
|
|
||||||
|
target_library_name = task_config.get("mdb_name")
|
||||||
|
if not target_library_name:
|
||||||
|
print("飞牛影视: 未指定媒体库名称,跳过处理。")
|
||||||
|
return
|
||||||
|
target_library_mdb_dir_list = task_config.get("mdb_dir_list")
|
||||||
|
dir_list = []
|
||||||
|
if target_library_mdb_dir_list:
|
||||||
|
dir_list = [dir_path.strip() for dir_path in target_library_mdb_dir_list.split(",") if dir_path.strip()]
|
||||||
|
|
||||||
|
# 获取媒体库ID
|
||||||
|
library_id = self._get_library_id(target_library_name)
|
||||||
|
|
||||||
|
if library_id:
|
||||||
|
# 获取ID成功后,刷新该媒体库
|
||||||
|
self._refresh_library(library_id, dir_list=dir_list)
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# Internal Methods (内部实现方法)
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
def _check_config(self):
|
||||||
|
"""检查配置是否完整"""
|
||||||
|
missing_keys = [
|
||||||
|
key for key in self.default_config
|
||||||
|
if key not in self.OPTIONAL_KEYS and not getattr(self, key, None)
|
||||||
|
]
|
||||||
|
if missing_keys:
|
||||||
|
# print(f"{self.plugin_name} 模块缺少必要参数: {', '.join(missing_keys)}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _make_request(self, method: str, rel_url: str, params: dict = None, data: dict = None) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
一个统一的私有方法,用于发送所有API请求。
|
||||||
|
它会自动处理签名、请求头、错误和响应解析。
|
||||||
|
当认证失败时,会自动尝试重新登录并重试,最多3次。
|
||||||
|
"""
|
||||||
|
max_retries = 3
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
url = f"{self.base_url.rstrip('/')}{rel_url}"
|
||||||
|
|
||||||
|
authx = self._cse_sign(method, rel_url, params, data)
|
||||||
|
if not authx:
|
||||||
|
print(f"飞牛影视: 为 {rel_url} 生成签名失败,请求中止。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"authx": authx,
|
||||||
|
}
|
||||||
|
if self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.session.request(
|
||||||
|
method, url, headers=headers, params=params,
|
||||||
|
data=self._serialize_data(data if data is not None else {})
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
response_data = response.json()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"飞牛影视: 请求 {url} 时出错: {e}")
|
||||||
|
return None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"飞牛影视: 解析来自 {url} 的响应失败,内容非JSON格式。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
response_code = response_data.get("code")
|
||||||
|
if response_code is None:
|
||||||
|
print(f"飞牛影视: 响应格式错误,未找到 'code' 字段。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if response_code == 0:
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
if response_code == -2:
|
||||||
|
print(f"飞牛影视: 认证失败 (尝试 {attempt + 1}/{max_retries}),尝试重新登录...")
|
||||||
|
if rel_url == self.API_LOGIN:
|
||||||
|
print("飞牛影视: 登录接口认证失败,请检查用户名和密码。")
|
||||||
|
return response_data
|
||||||
|
if not self._login():
|
||||||
|
print("飞牛影视: 重新登录失败,无法继续请求。")
|
||||||
|
return None
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
msg = response_data.get('msg', '未知错误')
|
||||||
|
print(f"飞牛影视: API调用失败 ({rel_url}): {msg}")
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
print(f"飞牛影视: 请求 {rel_url} 在尝试 {max_retries} 次后仍然失败。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _login(self) -> bool:
|
||||||
|
"""
|
||||||
|
登录到飞牛影视服务器并获取认证token。
|
||||||
|
"""
|
||||||
|
app_name = self.app_name or self.default_config["app_name"]
|
||||||
|
username = self.username or self.default_config["username"]
|
||||||
|
password = self.password or self.default_config["password"]
|
||||||
|
print("飞牛影视: 正在尝试登录...")
|
||||||
|
|
||||||
|
payload = {"username": username, "password": password, "app_name": app_name}
|
||||||
|
response_json = self._make_request('post', self.API_LOGIN, data=payload)
|
||||||
|
|
||||||
|
if response_json and response_json.get("data", {}).get("token"):
|
||||||
|
self.token = response_json["data"]["token"]
|
||||||
|
print("飞牛影视: 登录成功 ✅")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("飞牛影视: 登录失败 ❌")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_library_id(self, library_name: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
根据媒体库的名称获取其唯一ID (guid)。
|
||||||
|
"""
|
||||||
|
if not self.token:
|
||||||
|
print("飞牛影视: 必须先登录才能获取媒体库列表。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"飞牛影视: 正在查找媒体库 '{library_name}'...")
|
||||||
|
response_json = self._make_request('get', self.API_MDB_LIST)
|
||||||
|
|
||||||
|
if response_json and response_json.get("data"):
|
||||||
|
for library in response_json.get("data", []):
|
||||||
|
if library.get("name") == library_name:
|
||||||
|
print(f"飞牛影视: 找到目标媒体库 ✅,ID: {library.get('guid')}")
|
||||||
|
return library.get("guid")
|
||||||
|
print(f"飞牛影视: 未在媒体库列表中找到名为 '{library_name}' 的媒体库 ❌")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _refresh_library(self, library_id: str, dir_list: list[str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
根据给定的媒体库ID触发一次媒体库扫描/刷新。
|
||||||
|
"""
|
||||||
|
if not self.token:
|
||||||
|
print("飞牛影视: 必须先登录才能刷新媒体库。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if dir_list:
|
||||||
|
print(f"飞牛影视: 正在为媒体库 {library_id} 发送部分目录{dir_list}刷新指令...")
|
||||||
|
else:
|
||||||
|
print(f"飞牛影视: 正在为媒体库 {library_id} 发送刷新指令...")
|
||||||
|
rel_url = self.API_MDB_SCAN.format(library_id)
|
||||||
|
request_body = {"dir_list": dir_list} if dir_list else {}
|
||||||
|
response_json = self._make_request('post', rel_url, data=request_body)
|
||||||
|
|
||||||
|
if not response_json: return False
|
||||||
|
|
||||||
|
response_code = response_json.get("code")
|
||||||
|
if response_code == 0:
|
||||||
|
print(f"飞牛影视: 发送刷新指令成功 ✅")
|
||||||
|
return True
|
||||||
|
elif response_code == -14:
|
||||||
|
if self._stop_refresh_task(library_id):
|
||||||
|
print(f"飞牛影视: 发现重复任务,已停止旧任务,重新发送刷新指令...")
|
||||||
|
response_json = self._make_request('post', rel_url, data={})
|
||||||
|
if response_json and response_json.get("code") == 0:
|
||||||
|
print(f"飞牛影视: 发送刷新指令成功 ✅")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"飞牛影视: 重新发送刷新指令失败 ❌")
|
||||||
|
else:
|
||||||
|
print(f"飞牛影视: 停止旧任务失败,无法继续刷新操作 ❌")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _stop_refresh_task(self, library_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
停止指定的媒体库刷新任务。
|
||||||
|
"""
|
||||||
|
if not self.token:
|
||||||
|
print("飞牛影视: 必须先登录才能停止刷新任务。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"飞牛影视: 正在停止媒体库刷新任务 {library_id}...")
|
||||||
|
payload = {"guid": library_id, "type": "TaskItemScrap"}
|
||||||
|
response_json = self._make_request('post', self.API_TASK_STOP, data=payload)
|
||||||
|
|
||||||
|
if response_json and response_json.get("code") == 0:
|
||||||
|
print(f"飞牛影视: 停止刷新任务成功 ✅")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"飞牛影视: 停止刷新任务失败 ❌")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _cse_sign(self, method: str, path: str, params: dict = None, data: dict = None) -> str:
|
||||||
|
"""
|
||||||
|
为API请求生成 cse 签名参数字符串。
|
||||||
|
"""
|
||||||
|
nonce = str(random.randint(100000, 999999))
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
|
||||||
|
serialized_str = ""
|
||||||
|
if method.lower() == 'get':
|
||||||
|
if params:
|
||||||
|
serialized_str = urlencode(sorted(params.items()))
|
||||||
|
else:
|
||||||
|
serialized_str = self._serialize_data(data)
|
||||||
|
body_hash = self._md5_hash(serialized_str)
|
||||||
|
|
||||||
|
string_to_sign_parts = [
|
||||||
|
self.secret_string, path, nonce, timestamp, body_hash, self.api_key
|
||||||
|
]
|
||||||
|
string_to_sign = "_".join(string_to_sign_parts)
|
||||||
|
final_sign = self._md5_hash(string_to_sign)
|
||||||
|
|
||||||
|
return f"nonce={nonce}×tamp={timestamp}&sign={final_sign}"
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# Static Utility Methods (静态工具方法)
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _md5_hash(s: str) -> str:
|
||||||
|
"""计算并返回字符串的小写 MD5 哈希值。"""
|
||||||
|
return hashlib.md5(s.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _serialize_data(data: Any) -> str:
|
||||||
|
"""
|
||||||
|
将请求体数据序列化为紧凑的JSON字符串。
|
||||||
|
"""
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return json.dumps(data, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
|
||||||
|
if isinstance(data, str):
|
||||||
|
return data
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
return ""
|
||||||
BIN
plugins/fnv_refresh_v2.so
Normal file
@ -23,7 +23,7 @@ class Plex:
|
|||||||
if self.get_info():
|
if self.get_info():
|
||||||
self.is_active = True
|
self.is_active = True
|
||||||
|
|
||||||
def run(self, task):
|
def run(self, task, **kwargs):
|
||||||
if task.get("savepath"):
|
if task.get("savepath"):
|
||||||
# 检查是否已缓存库信息
|
# 检查是否已缓存库信息
|
||||||
if self._libraries is None:
|
if self._libraries is None:
|
||||||
@ -90,7 +90,6 @@ class Plex:
|
|||||||
return libraries
|
return libraries
|
||||||
else:
|
else:
|
||||||
print(f"🎞️ 获取Plex媒体库信息失败❌ 状态码:{response.status_code}")
|
print(f"🎞️ 获取Plex媒体库信息失败❌ 状态码:{response.status_code}")
|
||||||
return None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取Plex媒体库信息出错: {e}")
|
print(f"获取Plex媒体库信息出错: {e}")
|
||||||
return None
|
return []
|
||||||
75
plugins/smartstrm.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class Smartstrm:
|
||||||
|
default_config = {
|
||||||
|
"webhook": "", # SmartStrm Webhook 地址
|
||||||
|
"strmtask": "", # SmartStrm 任务名,支持多个如 `tv,movie`
|
||||||
|
"xlist_path_fix": "", # 路径映射, SmartStrm 任务使用 quark 驱动时无须填写;使用 openlist 驱动时需填写 `/storage_mount_path:/quark_root_dir` ,例如把夸克根目录挂载在 OpenList 的 /quark 下,则填写 `/quark:/` ;以及 SmartStrm 会使 OpenList 强制刷新目录,无需再用 alist 插件刷新。
|
||||||
|
}
|
||||||
|
is_active = False
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.plugin_name = self.__class__.__name__.lower()
|
||||||
|
if kwargs:
|
||||||
|
for key, _ in self.default_config.items():
|
||||||
|
if key in kwargs:
|
||||||
|
setattr(self, key, kwargs[key])
|
||||||
|
else:
|
||||||
|
print(f"{self.plugin_name} 模块缺少必要参数: {key}")
|
||||||
|
if self.webhook and self.strmtask:
|
||||||
|
if self.get_info():
|
||||||
|
self.is_active = True
|
||||||
|
|
||||||
|
def get_info(self):
|
||||||
|
"""获取 SmartStrm 信息"""
|
||||||
|
try:
|
||||||
|
response = requests.request(
|
||||||
|
"GET",
|
||||||
|
self.webhook,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
response = response.json()
|
||||||
|
if response.get("success"):
|
||||||
|
print(f"SmartStrm 触发任务: 连接成功 {response.get('version','')}")
|
||||||
|
return response
|
||||||
|
print(f"SmartStrm 触发任务:连接失败 {response.get('message','')}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SmartStrm 触发任务:连接出错 {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run(self, task, **kwargs):
|
||||||
|
"""
|
||||||
|
插件主入口函数
|
||||||
|
:param task: 任务配置
|
||||||
|
:param kwargs: 其他参数
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 准备发送的数据
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"event": "qas_strm",
|
||||||
|
"data": {
|
||||||
|
"strmtask": self.strmtask,
|
||||||
|
"savepath": task["savepath"],
|
||||||
|
"xlist_path_fix": self.xlist_path_fix,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# 发送 POST 请求
|
||||||
|
response = requests.request(
|
||||||
|
"POST",
|
||||||
|
self.webhook,
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
response = response.json()
|
||||||
|
if response.get("success"):
|
||||||
|
print(
|
||||||
|
f"SmartStrm 触发任务: [{response['task']['name']}] {response['task']['storage_path']} 成功✅"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"SmartStrm 触发任务: {response['message']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SmartStrm 触发任务:出错 {str(e)}")
|
||||||
1136
quark_auto_save.py
@ -6,27 +6,51 @@
|
|||||||
"QUARK_SIGN_NOTIFY": true,
|
"QUARK_SIGN_NOTIFY": true,
|
||||||
"其他推送渠道//此项可删": "配置方法同青龙"
|
"其他推送渠道//此项可删": "配置方法同青龙"
|
||||||
},
|
},
|
||||||
"media_servers": {
|
"plugins": {
|
||||||
"emby": {
|
"emby": {
|
||||||
"url": "",
|
"url": "",
|
||||||
"token": ""
|
"token": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"magic_regex": {
|
||||||
|
"$TV_REGEX": {
|
||||||
|
"pattern": ".*?([Ss]\\d{1,2})?(?:[第EePpXx\\.\\-\\_\\( ]{1,2}|^)(\\d{1,3})(?!\\d).*?\\.(mp4|mkv)",
|
||||||
|
"replace": "\\1E\\2.\\3"
|
||||||
|
},
|
||||||
|
"$BLACK_WORD": {
|
||||||
|
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*",
|
||||||
|
"replace": ""
|
||||||
|
},
|
||||||
|
"$SHOW_MAGIC": {
|
||||||
|
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*",
|
||||||
|
"replace": "{TASKNAME}.{SXX}E{II}.第{E}期{PART}.{EXT}"
|
||||||
|
},
|
||||||
|
"$TV_MAGIC": {
|
||||||
|
"pattern": ".*\\.(mp4|mkv|mov|m4v|avi|mpeg|ts)$",
|
||||||
|
"replace": "{TASKNAME}.{SXX}E{E}.{EXT}"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tasklist": [
|
"tasklist": [
|
||||||
{
|
{
|
||||||
"taskname": "测试-魔法匹配剧集(这是一组有效分享,配置CK后可测试任务是否正常)",
|
"taskname": "测试-魔法匹配剧集(这是一组有效分享,配置CK后可测试任务是否正常)",
|
||||||
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试",
|
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试",
|
||||||
"savepath": "/夸克自动转存测试",
|
"savepath": "/夸克自动转存测试/剧集",
|
||||||
"pattern": "$TV",
|
"pattern": "$TV_REGEX",
|
||||||
"replace": "",
|
"replace": "",
|
||||||
"enddate": "2099-01-30",
|
"enddate": "2099-01-30",
|
||||||
"media_id": "",
|
|
||||||
"update_subdir": "4k|1080p"
|
"update_subdir": "4k|1080p"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"taskname": "测试-广告过滤",
|
"taskname": "测试-综艺命名",
|
||||||
|
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-%E5%A4%B8%E5%85%8B%E8%87%AA%E5%8A%A8%E8%BD%AC%E5%AD%98%E6%B5%8B%E8%AF%95/71df3902f42d4270a58c0eb12aa2b014-%E7%BB%BC%E8%89%BA%E5%91%BD%E5%90%8D",
|
||||||
|
"savepath": "/夸克自动转存测试/综艺命名",
|
||||||
|
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*",
|
||||||
|
"replace": "{II}.{TASKNAME}.{DATE}.第{E}期{PART}.{EXT}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"taskname": "测试-去广告字符",
|
||||||
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试/680d91e490814da0927c38b432f88edc-带广告文件夹",
|
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试/680d91e490814da0927c38b432f88edc-带广告文件夹",
|
||||||
"savepath": "/夸克自动转存测试/带广告文件夹",
|
"savepath": "/夸克自动转存测试/去广告字符",
|
||||||
"pattern": "【XX电影网】(.*)\\.(mp4|mkv)",
|
"pattern": "【XX电影网】(.*)\\.(mp4|mkv)",
|
||||||
"replace": "\\1.\\2",
|
"replace": "\\1.\\2",
|
||||||
"enddate": "2099-01-30"
|
"enddate": "2099-01-30"
|
||||||
@ -34,7 +58,7 @@
|
|||||||
{
|
{
|
||||||
"taskname": "测试-超期任务",
|
"taskname": "测试-超期任务",
|
||||||
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试",
|
"shareurl": "https://pan.quark.cn/s/d07a34a9c695#/list/share/7e25ddd87cf64443b637125478733295-夸克自动转存测试",
|
||||||
"savepath": "/夸克自动转存测试",
|
"savepath": "/夸克自动转存测试/不会运行",
|
||||||
"pattern": "",
|
"pattern": "",
|
||||||
"replace": "",
|
"replace": "",
|
||||||
"enddate": "2000-01-30",
|
"enddate": "2000-01-30",
|
||||||
|
|||||||
@ -2,3 +2,4 @@ flask
|
|||||||
apscheduler
|
apscheduler
|
||||||
requests
|
requests
|
||||||
treelib
|
treelib
|
||||||
|
natsort
|
||||||