mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-12 15:20:44 +08:00
Compare commits
63 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 |
15
Dockerfile
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
|
||||||
|
|
||||||
|
|||||||
74
README.md
74
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,7 +29,7 @@
|
|||||||
> ⛔️⛔️⛔️ 注意!资源不会每时每刻更新,**严禁设定过高的定时运行频率!** 以免账号风控和给夸克服务器造成不必要的压力。雪山崩塌,每一片雪花都有责任!
|
> ⛔️⛔️⛔️ 注意!资源不会每时每刻更新,**严禁设定过高的定时运行频率!** 以免账号风控和给夸克服务器造成不必要的压力。雪山崩塌,每一片雪花都有责任!
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 开发者≠客服,开源免费≠帮你解决使用问题;本项目Wiki和已经相对完善,遇到问题请先翻阅 Issues 和 Wiki ,请勿盲目发问。
|
> 开发者≠客服,开源免费≠帮你解决使用问题;本项目 Wiki 已经相对完善,遇到问题请先翻阅 Issues 和 Wiki ,请勿盲目发问。
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
@ -58,7 +58,7 @@
|
|||||||
- 媒体库整合
|
- 媒体库整合
|
||||||
- [x] 根据任务名搜索 Emby 媒体库
|
- [x] 根据任务名搜索 Emby 媒体库
|
||||||
- [x] 追更或整理后自动刷新 Emby 媒体库
|
- [x] 追更或整理后自动刷新 Emby 媒体库
|
||||||
- [x] 媒体库模块化,用户可很方便地[开发自己的媒体库hook模块](./plugins)
|
- [x] 插件模块化,允许自行开发和挂载[插件](./plugins)
|
||||||
|
|
||||||
- 其它
|
- 其它
|
||||||
- [x] 每日签到领空间 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间)</sup>
|
- [x] 每日签到领空间 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间)</sup>
|
||||||
@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
### Docker 部署
|
### Docker 部署
|
||||||
|
|
||||||
Docker 部署提供 WebUI 管理配置,图形化配置已能满足绝大多数需求。部署命令:
|
Docker 部署提供 WebUI 进行管理配置,部署命令:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d \
|
docker run -d \
|
||||||
@ -108,10 +108,12 @@ 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` 禁用某些插件 |
|
| `PLUGIN_FLAGS` | | 插件标志,如 `-emby,-aria2` 禁用某些插件 |
|
||||||
|
| `TASK_TIMEOUT` | `1800` | 任务执行超时时间(秒),超时则任务结束 |
|
||||||
|
|
||||||
#### 一键更新
|
#### 一键更新
|
||||||
|
|
||||||
@ -128,32 +130,28 @@ 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 |
|
||||||
|
|
||||||
|
更多正则使用说明:[正则处理教程](https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程)
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
>
|
>
|
||||||
> **魔法匹配**:当任务 `pattern` 值为 `$开头` 且 `replace` 留空时,实际将调用程序预设的正则表达式。
|
> **魔法匹配和魔法变量**:在正则处理中,我们定义了一些“魔法匹配”模式,如果 表达式 的值以 $ 开头且 替换式 留空,程序将自动使用预设的正则表达式进行匹配和替换。
|
||||||
>
|
>
|
||||||
> 如 `$TV` 可适配和自动整理市面上90%分享剧集的文件名格式,具体实现见代码,欢迎贡献规则。
|
> 自 v0.6.0 开始,支持更多以 {} 包裹的我称之为“魔法变量”,可以更灵活地进行重命名。
|
||||||
|
>
|
||||||
更多正则使用说明:[正则处理教程](https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程)
|
> 更多说明请看[魔法匹配和魔法变量](https://github.com/Cp0204/quark-auto-save/wiki/魔法匹配和魔法变量)
|
||||||
|
|
||||||
### 刷新媒体库
|
### 刷新媒体库
|
||||||
|
|
||||||
@ -165,6 +163,40 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
|
|||||||
|
|
||||||
请参考 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块钱,让我知道开源有价值。谢谢!
|
||||||
@ -178,3 +210,9 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
|
|||||||
程序没有任何破解行为,只是对于夸克已有的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>
|
||||||
146
app/run.py
146
app/run.py
@ -15,12 +15,15 @@ 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.cloudsaver import CloudSaver
|
||||||
|
from sdk.pansou import PanSou
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import subprocess
|
import subprocess
|
||||||
import requests
|
import requests
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
import base64
|
import base64
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@ -30,10 +33,30 @@ 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, Config, MagicRename
|
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:
|
||||||
@ -50,6 +73,7 @@ PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "")
|
|||||||
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
|
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
|
||||||
HOST = os.environ.get("HOST", "0.0.0.0")
|
HOST = os.environ.get("HOST", "0.0.0.0")
|
||||||
PORT = os.environ.get("PORT", 5005)
|
PORT = os.environ.get("PORT", 5005)
|
||||||
|
TASK_TIMEOUT = int(os.environ.get("TASK_TIMEOUT", 1800))
|
||||||
|
|
||||||
config_data = {}
|
config_data = {}
|
||||||
task_plugins_config_default = {}
|
task_plugins_config_default = {}
|
||||||
@ -73,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):
|
||||||
@ -233,8 +259,19 @@ def get_task_suggestions():
|
|||||||
return jsonify({"success": False, "message": "未登录"})
|
return jsonify({"success": False, "message": "未登录"})
|
||||||
query = request.args.get("q", "").lower()
|
query = request.args.get("q", "").lower()
|
||||||
deep = request.args.get("d", "").lower()
|
deep = request.args.get("d", "").lower()
|
||||||
try:
|
net_data = config_data.get("source", {}).get("net", {})
|
||||||
cs_data = config_data.get("source", {}).get("cloudsaver", {})
|
cs_data = config_data.get("source", {}).get("cloudsaver", {})
|
||||||
|
ps_data = config_data.get("source", {}).get("pansou", {})
|
||||||
|
|
||||||
|
def net_search():
|
||||||
|
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 (
|
if (
|
||||||
cs_data.get("server")
|
cs_data.get("server")
|
||||||
and cs_data.get("username")
|
and cs_data.get("username")
|
||||||
@ -252,18 +289,37 @@ def get_task_suggestions():
|
|||||||
cs_data["token"] = search.get("new_token")
|
cs_data["token"] = search.get("new_token")
|
||||||
Config.write_json(CONFIG_PATH, config_data)
|
Config.write_json(CONFIG_PATH, config_data)
|
||||||
search_results = cs.clean_search_results(search.get("data"))
|
search_results = cs.clean_search_results(search.get("data"))
|
||||||
return jsonify(
|
return search_results
|
||||||
{"success": True, "source": "CloudSaver", "data": search_results}
|
return []
|
||||||
)
|
|
||||||
else:
|
def ps_search():
|
||||||
return jsonify({"success": True, "message": search.get("message")})
|
if ps_data.get("server"):
|
||||||
else:
|
ps = PanSou(ps_data.get("server"))
|
||||||
base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
|
return ps.search(query, deep == "1")
|
||||||
url = f"{base_url}/task_suggestions?q={query}&d={deep}"
|
return []
|
||||||
response = requests.get(url)
|
|
||||||
return jsonify(
|
try:
|
||||||
{"success": True, "source": "网络公开", "data": response.json()}
|
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:
|
except Exception as e:
|
||||||
return jsonify({"success": True, "message": f"error: {str(e)}"})
|
return jsonify({"success": True, "message": f"error: {str(e)}"})
|
||||||
|
|
||||||
@ -284,7 +340,9 @@ def get_share_detail():
|
|||||||
return jsonify(
|
return jsonify(
|
||||||
{"success": False, "data": {"error": get_stoken.get("message")}}
|
{"success": False, "data": {"error": get_stoken.get("message")}}
|
||||||
)
|
)
|
||||||
share_detail = account.get_detail(pwd_id, stoken, pdir_fid, _fetch_share=1)
|
share_detail = account.get_detail(
|
||||||
|
pwd_id, stoken, pdir_fid, _fetch_share=1, fetch_share_full_path=1
|
||||||
|
)
|
||||||
|
|
||||||
if share_detail.get("code") != 0:
|
if share_detail.get("code") != 0:
|
||||||
return jsonify(
|
return jsonify(
|
||||||
@ -292,7 +350,10 @@ def get_share_detail():
|
|||||||
)
|
)
|
||||||
|
|
||||||
data = share_detail["data"]
|
data = share_detail["data"]
|
||||||
data["paths"] = paths
|
data["paths"] = [
|
||||||
|
{"fid": i["fid"], "name": i["file_name"]}
|
||||||
|
for i in share_detail["data"].get("full_path", [])
|
||||||
|
] or paths
|
||||||
data["stoken"] = stoken
|
data["stoken"] = stoken
|
||||||
|
|
||||||
# 正则处理预览
|
# 正则处理预览
|
||||||
@ -310,14 +371,16 @@ def get_share_detail():
|
|||||||
dir_file_list = []
|
dir_file_list = []
|
||||||
dir_filename_list = []
|
dir_filename_list = []
|
||||||
|
|
||||||
for share_file in data["list"]:
|
|
||||||
if share_file["dir"] and task.get("update_subdir", False):
|
|
||||||
pattern, replace = task["update_subdir"], ""
|
|
||||||
else:
|
|
||||||
pattern, replace = mr.magic_regex_conv(
|
pattern, replace = mr.magic_regex_conv(
|
||||||
task.get("pattern", ""), task.get("replace", "")
|
task.get("pattern", ""), task.get("replace", "")
|
||||||
)
|
)
|
||||||
if re.search(pattern, share_file["file_name"]):
|
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 = (
|
file_name_re = (
|
||||||
share_file["file_name"]
|
share_file["file_name"]
|
||||||
@ -424,7 +487,36 @@ def add_task():
|
|||||||
# 定时任务执行的函数
|
# 定时任务执行的函数
|
||||||
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 函数执行完成")
|
||||||
|
|
||||||
|
|
||||||
# 重新加载任务
|
# 重新加载任务
|
||||||
@ -440,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()
|
||||||
@ -458,7 +554,7 @@ def reload_tasks():
|
|||||||
|
|
||||||
def init():
|
def init():
|
||||||
global config_data, task_plugins_config_default
|
global config_data, task_plugins_config_default
|
||||||
logging.info(f">>> 初始化配置")
|
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)):
|
||||||
@ -496,6 +592,8 @@ def init():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
init()
|
init()
|
||||||
reload_tasks()
|
reload_tasks()
|
||||||
|
logging.info(">>> 启动Web服务")
|
||||||
|
logging.info(f"运行在: http://{HOST}:{PORT}")
|
||||||
app.run(
|
app.run(
|
||||||
debug=DEBUG,
|
debug=DEBUG,
|
||||||
host=HOST,
|
host=HOST,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
|
from sdk.common import iso_to_cst
|
||||||
|
|
||||||
|
|
||||||
class CloudSaver:
|
class CloudSaver:
|
||||||
@ -124,6 +125,10 @@ class CloudSaver:
|
|||||||
content = content.replace('<mark class="highlight">', "")
|
content = content.replace('<mark class="highlight">', "")
|
||||||
content = content.replace("</mark>", "")
|
content = content.replace("</mark>", "")
|
||||||
content = content.strip()
|
content = content.strip()
|
||||||
|
# 统一发布时间格式
|
||||||
|
pubdate = item.get("pubDate", "")
|
||||||
|
if pubdate:
|
||||||
|
pubdate = iso_to_cst(pubdate)
|
||||||
# 链接去重
|
# 链接去重
|
||||||
if link.get("link") not in link_array:
|
if link.get("link") not in link_array:
|
||||||
link_array.append(link.get("link"))
|
link_array.append(link.get("link"))
|
||||||
@ -132,9 +137,10 @@ class CloudSaver:
|
|||||||
"shareurl": link.get("link"),
|
"shareurl": link.get("link"),
|
||||||
"taskname": title,
|
"taskname": title,
|
||||||
"content": content,
|
"content": content,
|
||||||
|
"datetime": pubdate,
|
||||||
"tags": item.get("tags", []),
|
"tags": item.get("tags", []),
|
||||||
"channel": item.get("channel", ""),
|
"channel": item.get("channelId", ""),
|
||||||
"channel_id": item.get("channelId", ""),
|
"source": "CloudSaver"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return clean_results
|
return clean_results
|
||||||
|
|||||||
16
app/sdk/common.py
Normal file
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
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)
|
||||||
@ -45,7 +45,7 @@ body {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.jsoneditor-tree > tbody > tr.jsoneditor-expandable:first-child {
|
table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,3 +196,48 @@ table.jsoneditor-tree > tbody > tr.jsoneditor-expandable:first-child {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
// @name QAS一键推送助手
|
// @name QAS一键推送助手
|
||||||
// @namespace https://github.com/Cp0204/quark-auto-save
|
// @namespace https://github.com/Cp0204/quark-auto-save
|
||||||
// @license AGPL
|
// @license AGPL
|
||||||
// @version 0.4
|
// @version 0.6
|
||||||
// @description 在夸克网盘分享页面添加推送到 QAS 的按钮
|
// @description 在夸克网盘分享页面添加推送到 QAS 的按钮
|
||||||
// @icon https://pan.quark.cn/favicon.ico
|
// @icon https://pan.quark.cn/favicon.ico
|
||||||
// @author Cp0204
|
// @author Cp0204
|
||||||
@ -30,11 +30,11 @@
|
|||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
html: `
|
html: `
|
||||||
<label for="qas_base">QAS 地址</label>
|
<label for="qas_base">QAS 地址</label>
|
||||||
<input id="qas_base" class="swal2-input" placeholder="如: http://192.168.1.8:5005" value="${qas_base}">
|
<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>
|
<label for="qas_token">QAS Token</label>
|
||||||
<input id="qas_token" class="swal2-input" placeholder="v0.5+ 系统配置中查找" value="${qas_token}">
|
<input id="qas_token" class="swal2-input" placeholder="v0.5+ 系统配置中查找" value="${qas_token}"><br>
|
||||||
<label for="qas_token">默认正则</label>
|
<label for="qas_token">默认正则</label>
|
||||||
<input id="default_pattern" class="swal2-input" placeholder="如 $TV" value="${default_pattern}">
|
<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}">
|
<label for="qas_token">默认替换</label><input id="default_replace" class="swal2-input" value="${default_replace}">
|
||||||
`,
|
`,
|
||||||
focusConfirm: false,
|
focusConfirm: false,
|
||||||
@ -76,16 +76,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForElement('.DetailLayout--client-download--FpyCkdW.ant-dropdown-trigger', (clientDownloadButton) => {
|
waitForElement('.pc-member-entrance', (PcMemberButton) => {
|
||||||
const qasSettingButton = document.createElement('div');
|
const qasSettingButton = document.createElement('div');
|
||||||
qasSettingButton.className = 'DetailLayout--client-download--FpyCkdW ant-dropdown-trigger';
|
qasSettingButton.className = 'pc-member-entrance';
|
||||||
qasSettingButton.innerHTML = 'QAS设置';
|
qasSettingButton.innerHTML = 'QAS设置';
|
||||||
|
|
||||||
qasSettingButton.addEventListener('click', () => {
|
qasSettingButton.addEventListener('click', () => {
|
||||||
showQASSettingDialog();
|
showQASSettingDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
clientDownloadButton.parentNode.insertBefore(qasSettingButton, clientDownloadButton.nextSibling);
|
PcMemberButton.parentNode.insertBefore(qasSettingButton, PcMemberButton.nextSibling);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +112,8 @@
|
|||||||
// 获取数据函数
|
// 获取数据函数
|
||||||
function getData() {
|
function getData() {
|
||||||
const currentUrl = window.location.href;
|
const currentUrl = window.location.href;
|
||||||
taskname = currentUrl.lastIndexOf('-') > 0 ? decodeURIComponent(currentUrl.match(/.*\/[^-]+-(.+)$/)[1]).replace('*101', '-') : document.querySelector('.author-name').textContent;
|
const lastTitle = document.querySelector('.primary .bcrumb-filename:last-child')?.getAttribute('title') || null;
|
||||||
|
taskname = (lastTitle && lastTitle != "全部文件") ? lastTitle : document.querySelector('.author-name').textContent;
|
||||||
shareurl = currentUrl;
|
shareurl = currentUrl;
|
||||||
let pathElement = document.querySelector('.path-name');
|
let pathElement = document.querySelector('.path-name');
|
||||||
savepath = pathElement ? pathElement.title.replace('全部文件', '').trim() : "";
|
savepath = pathElement ? pathElement.title.replace('全部文件', '').trim() : "";
|
||||||
@ -154,6 +155,63 @@
|
|||||||
},
|
},
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
onload: function (response) {
|
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 {
|
try {
|
||||||
const jsonResponse = JSON.parse(response.responseText);
|
const jsonResponse = JSON.parse(response.responseText);
|
||||||
if (jsonResponse.success) {
|
if (jsonResponse.success) {
|
||||||
@ -176,16 +234,34 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: '解析响应失败',
|
title: '解析响应失败',
|
||||||
text: `无法解析 JSON 响应: ${response.responseText}`,
|
html: `<small>
|
||||||
icon: 'error'
|
响应状态: ${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) {
|
onerror: function (error) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: '任务创建失败',
|
title: '网络请求失败',
|
||||||
text: error,
|
text: '无法连接到 QAS 服务器,请检查网络连接和服务器地址',
|
||||||
icon: 'error'
|
icon: 'error',
|
||||||
|
confirmButtonText: '重新配置',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
showQASSettingDialog();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -77,7 +77,7 @@
|
|||||||
<button type="button" class="btn btn-outline-primary" @click="addCookie()">+</button>
|
<button type="button" class="btn btn-outline-primary" @click="addCookie()">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>1. 所有账号执行签到,纯签到只需移动端参数即可!</p>
|
<p>1. 所有账号执行签到,纯<a class="" href="https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间">签到</a>只需移动端参数即可!</p>
|
||||||
<p>2. 仅第一个账号执行转存,请自行确认顺序。<b>最好是手机验证码<a target="_blank" href="https://pan.quark.cn/">登录</a>,CK比较完整!</b>如需签到参数附在CK后面。</p>
|
<p>2. 仅第一个账号执行转存,请自行确认顺序。<b>最好是手机验证码<a target="_blank" href="https://pan.quark.cn/">登录</a>,CK比较完整!</b>如需签到参数附在CK后面。</p>
|
||||||
<div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2">
|
<div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2">
|
||||||
<input type="text" v-model="formData.cookie[index]" class="form-control" placeholder="打开 pan.quark.com 按 F12 抓取">
|
<input type="text" v-model="formData.cookie[index]" class="form-control" placeholder="打开 pan.quark.com 按 F12 抓取">
|
||||||
@ -109,7 +109,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 text-right">
|
<div class="col-4 text-right">
|
||||||
<button type="button" class="btn btn-outline-success" title="通知推送测试" @click="testPush()"><i class="bi bi-lightning"></i></button>
|
<button type="button" class="btn btn-success" title="通知推送测试" @click="testPush()"><i class="bi bi-lightning"></i></button>
|
||||||
<button type="button" class="btn btn-outline-primary" @click="addPush()">+</button>
|
<button type="button" class="btn btn-outline-primary" @click="addPush()">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -198,12 +198,35 @@
|
|||||||
|
|
||||||
<div class="row title" title="资源搜索服务配置,用于任务名称智能搜索">
|
<div class="row title" title="资源搜索服务配置,用于任务名称智能搜索">
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
<h2 style="display: inline-block;"><i class="bi bi-search"></i> CloudSaver</h2>
|
<h2 style="display: inline-block;"><i class="bi bi-search"></i> 资源搜索</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row mb-0" style="display:flex; align-items:center;">
|
||||||
|
<div data-toggle="collapse" data-target="#collapse_net" aria-expanded="true" aria-controls="collapse_net">
|
||||||
|
<div class="btn btn-block text-left">
|
||||||
|
<i class="bi bi-caret-right-fill"></i> 网络公开搜索
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse show ml-3" id="collapse_net">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-2 col-form-label">启用</label>
|
||||||
|
<div class="col-sm-10 d-flex align-items-center">
|
||||||
|
<input type="checkbox" class="form-check-input" v-model="formData.source.net.enable" placeholder="是否启用网络公开搜索,默认启用">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row mb-0" style="display:flex; align-items:center;">
|
||||||
|
<div data-toggle="collapse" data-target="#collapse_cloudsaver" aria-expanded="true" aria-controls="collapse_cloudsaver">
|
||||||
|
<div class="btn btn-block text-left">
|
||||||
|
<i class="bi bi-caret-right-fill"></i> CloudSaver
|
||||||
<span class="badge badge-pill badge-light">
|
<span class="badge badge-pill badge-light">
|
||||||
<a href="https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源" target="_blank">?</a>
|
<a href="https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源" target="_blank">?</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse show ml-3" id="collapse_cloudsaver">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label">服务器</label>
|
<label class="col-sm-2 col-form-label">服务器</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
@ -222,6 +245,25 @@
|
|||||||
<input type="password" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
|
<input type="password" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row mb-0" style="display:flex; align-items:center;">
|
||||||
|
<div data-toggle="collapse" data-target="#collapse_pansou" aria-expanded="true" aria-controls="collapse_pansou">
|
||||||
|
<div class="btn btn-block text-left">
|
||||||
|
<i class="bi bi-caret-right-fill"></i> PanSou
|
||||||
|
<span class="badge badge-pill badge-light">
|
||||||
|
<a href="https://github.com/fish2018/pansou" target="_blank">?</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse show ml-3" id="collapse_pansou">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-2 col-form-label">服务器</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" v-model="formData.source.pansou.server" class="form-control" placeholder="资源搜索服务器地址,如 https://so.252035.xyz">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -268,9 +310,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button class="btn btn-warning" v-if="task.shareurl_ban" :title="task.shareurl_ban" disabled><i class="bi bi-exclamation-triangle-fill"></i></button>
|
<button type="button" class="btn btn-outline-primary btn-sm" @click="copyTaskToClipboard(index)" title="复制任务参数到粘贴板"><i class=" bi bi-clipboard-check-fill"></i></button>
|
||||||
<button type="button" class="btn btn-outline-primary" @click="runScriptNow(index)" title="运行此任务" v-else><i class="bi bi-play-fill"></i></button>
|
<button class="btn btn-warning btn-sm" v-if="task.shareurl_ban" :title="task.shareurl_ban" disabled><i class="bi bi-exclamation-triangle-fill"></i></button>
|
||||||
<button type="button" class="btn btn-outline-danger" @click="removeTask(index)" title="删除此任务"><i class="bi bi-trash3-fill"></i></button>
|
<button type="button" class="btn btn-outline-primary btn-sm" @click="runScriptNow(index)" title="运行此任务" v-else><i class="bi bi-play-fill"></i></button>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" @click="removeTask(index)" title="删除此任务"><i class="bi bi-trash3-fill"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse ml-3" :id="'collapse_'+index">
|
<div class="collapse ml-3" :id="'collapse_'+index">
|
||||||
@ -281,12 +324,15 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="taskname[]" class="form-control" v-model="task.taskname" placeholder="必填" @focus="smart_param.showSuggestions=true;focusTaskname(index, task)" @input="changeTaskname(index, task)">
|
<input type="text" name="taskname[]" class="form-control" v-model="task.taskname" placeholder="必填" @focus="smart_param.showSuggestions=true;focusTaskname(index, task)" @input="changeTaskname(index, task)">
|
||||||
<div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.taskSuggestions.success && smart_param.index === index">
|
<div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.taskSuggestions.success && smart_param.index === index">
|
||||||
<div class="dropdown-item text-muted text-center" style="font-size:12px;">{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data.length ? `以下资源来自 ${smart_param.taskSuggestions.source} 搜索,请自行辨识,如有侵权请联系资源方` : "未搜索到资源" }}</div>
|
<div class="dropdown-item text-muted text-center" style="font-size:12px;">{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data.length ? `以下资源来自网络搜索,请自行辨识,如有侵权请联系资源方` : "未搜索到资源" }}</div>
|
||||||
<div v-for="suggestion in smart_param.taskSuggestions.data" :key="suggestion.taskname" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(index, suggestion)" style="font-size: 12px;" :title="suggestion.content">
|
<div v-for="suggestion in smart_param.taskSuggestions.data" :key="suggestion.taskname" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(index, suggestion)" style="font-size: 12px;" :title="suggestion.content">
|
||||||
<span v-html="suggestion.verify ? '✅': '❔'"></span> {{ suggestion.taskname }}
|
<span v-html="suggestion.verify ? '✅': '❔'"></span> {{ suggestion.taskname }}
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<a :href="suggestion.shareurl" target="_blank" @click.stop>{{ suggestion.shareurl }}</a>
|
<a :href="suggestion.shareurl" target="_blank" @click.stop>{{ suggestion.shareurl }}</a>
|
||||||
</small>
|
</small>
|
||||||
|
<span class="badge bg-transparent border border-success text-success">{{ suggestion.source || "网络公开" }}</span>
|
||||||
|
<span class="badge bg-transparent border border-info text-info">{{ suggestion.channel }}</span>
|
||||||
|
<span v-if="suggestion.datetime" class="badge bg-transparent border border-dark text-dark">{{ suggestion.datetime }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group-append" title="深度搜索">
|
<div class="input-group-append" title="深度搜索">
|
||||||
@ -307,7 +353,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)">
|
<input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)">
|
||||||
<div class="input-group-append" v-if="task.shareurl">
|
<div class="input-group-append" v-if="task.shareurl">
|
||||||
<button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;showShareSelect(index)" title="选择文件夹"><i class="bi bi-folder"></i></button>
|
<button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.switchShare=false;fileSelect.previewRegex=false;fileSelect.sortBy='file_name';fileSelect.sortOrder='desc';showShareSelect(index)" title="选择文件夹"><i class="bi bi-folder"></i></button>
|
||||||
<div class="input-group-text">
|
<div class="input-group-text">
|
||||||
<a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a>
|
<a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||||
</div>
|
</div>
|
||||||
@ -322,7 +368,7 @@
|
|||||||
<input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(index, task)">
|
<input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(index, task)">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath"><i class="bi bi-reply"></i></button>
|
<button class="btn btn-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath"><i class="bi bi-reply"></i></button>
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="showSavepathSelect(index)">选择</button>
|
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.sortBy='file_name';fileSelect.sortOrder='asc';showSavepathSelect(index)">选择</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -332,9 +378,9 @@
|
|||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.previewRegex=true;showShareSelect(index)" title="预览正则处理效果">正则处理</button>
|
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.switchShare=false;fileSelect.previewRegex=true;fileSelect.sortBy='file_name';fileSelect.sortOrder='asc';showShareSelect(index)" title="预览正则处理效果">正则处理</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex">
|
<input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex" @dblclick="inputRawMagicRegex(task)" title="双击可将魔法匹配释放为填入原始正则表达式">
|
||||||
<input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式">
|
<input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式">
|
||||||
<div class="input-group-append" title="保存时只比较文件名的部分,01.mp4 和 01.mkv 视同为同一文件,不重复转存">
|
<div class="input-group-append" title="保存时只比较文件名的部分,01.mp4 和 01.mkv 视同为同一文件,不重复转存">
|
||||||
<div class="input-group-text">
|
<div class="input-group-text">
|
||||||
@ -353,15 +399,22 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" placeholder="可选,只转存修改日期>此文件的文件" name="startfid[]" v-model="task.startfid">
|
<input type="text" class="form-control" placeholder="可选,只转存修改日期>此文件的文件" name="startfid[]" v-model="task.startfid">
|
||||||
<div class="input-group-append" v-if="task.shareurl">
|
<div class="input-group-append" v-if="task.shareurl">
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;showShareSelect(index)">选择</button>
|
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.switchShare=false;fileSelect.previewRegex=false;fileSelect.sortBy='updated_at';fileSelect.sortOrder='desc';showShareSelect(index)">选择</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row" title="需匹配到各级嵌套目录名才会更新,否则子目录在第一次转存后不会更新。注意:原理是逐级索引,深层嵌套目录的场景下效率非常低,慎用 .*">
|
<div class="form-group row" title="需匹配到各级嵌套目录名才会更新,否则子目录在第一次转存后不会更新。注意:递归模式原理是逐级索引,深层嵌套目录的场景下效率非常低,慎用 .*">
|
||||||
<label class="col-sm-2 col-form-label">更新目录</label>
|
<label class="col-sm-2 col-form-label">更新目录</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
<div class="input-group">
|
||||||
<input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选,匹配需更新子目录(含各级嵌套目录)的正则表达式,多项以|分割,如 4k|1080p">
|
<input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选,匹配需更新子目录(含各级嵌套目录)的正则表达式,多项以|分割,如 4k|1080p">
|
||||||
|
<div class="input-group-append" title="重存模式:删除该目录下所有文件,重新转存,大资源包时推荐使用
不勾选为递归模式:递归检查,逐级更新嵌套目录,效率低">
|
||||||
|
<div class="input-group-text">
|
||||||
|
<input type="checkbox" v-model="task.update_subdir_resave_mode"> 重存模式
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
@ -394,7 +447,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col-sm-12 text-center">
|
<div class="col-sm-12 text-center">
|
||||||
|
<div class="btn-group" role="group" aria-label="任务操作">
|
||||||
<button type="button" class="btn btn-primary" @click="addTask()"><i class="bi bi-plus"></i> 增加任务</button>
|
<button type="button" class="btn btn-primary" @click="addTask()"><i class="bi bi-plus"></i> 增加任务</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="addTaskForClipboard()" title="从粘贴板导入"><i class="bi bi-clipboard-plus"></i></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -443,6 +499,34 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body small">
|
<div class="modal-body small">
|
||||||
|
<!-- 分享链接来源 -->
|
||||||
|
<div class="mb-3 row" v-if="fileSelect.switchShare">
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<div>
|
||||||
|
<b>名称:</b>
|
||||||
|
<span :title="fileSelect.share.content">{{ fileSelect.share.taskname }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>链接:</b>
|
||||||
|
<a :href="fileSelect.share.shareurl" target="_blank" @click.stop>{{ fileSelect.share.shareurl }}</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>来源:</b>
|
||||||
|
<span class="badge bg-transparent border border-success text-success">{{ fileSelect.share.source || "网络公开" }}</span>
|
||||||
|
<span class="badge bg-transparent border border-info text-info" v-if="fileSelect.share.channel">{{ fileSelect.share.channel }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="fileSelect.share.datetime">
|
||||||
|
<b>时间:</b>
|
||||||
|
<span>{{ fileSelect.share.datetime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 text-right">
|
||||||
|
<div class="btn-group" title="资源搜索结果切换">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" @click="switchShare(-1)">上一个</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" @click="switchShare(1)">下一个</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div>
|
<div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- 正则处理表达式 -->
|
<!-- 正则处理表达式 -->
|
||||||
@ -468,12 +552,24 @@
|
|||||||
<table class="table table-hover table-sm">
|
<table class="table table-hover table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">文件名</th>
|
<th scope="col" class="cursor-pointer" @click="sortFileList('file_name')">
|
||||||
<th scope="col" v-if="fileSelect.selectShare">正则处理</th>
|
文件名
|
||||||
|
<span v-if="fileSelect.sortBy === 'file_name'">{{ fileSelect.sortOrder === "asc" ? "↑" : "↓" }}</span>
|
||||||
|
</th>
|
||||||
|
<th scope="col" v-if="fileSelect.selectShare">
|
||||||
|
正则处理
|
||||||
|
</th>
|
||||||
<template v-if="!fileSelect.previewRegex">
|
<template v-if="!fileSelect.previewRegex">
|
||||||
<th scope="col">大小</th>
|
<th scope="col">
|
||||||
<th scope="col">修改日期 ↓</th>
|
大小
|
||||||
<th scope="col" v-if="!fileSelect.selectShare">操作</th>
|
</th>
|
||||||
|
<th scope="col" class="cursor-pointer" @click="sortFileList('updated_at')">
|
||||||
|
修改日期
|
||||||
|
<span v-if="fileSelect.sortBy === 'updated_at'">{{ fileSelect.sortOrder === "asc" ? "↑" : "↓" }}</span>
|
||||||
|
</th>
|
||||||
|
<th scope="col" v-if="!fileSelect.selectShare">
|
||||||
|
操作
|
||||||
|
</th>
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -485,7 +581,7 @@
|
|||||||
<td v-if="file.dir">{{ file.include_items }}项</td>
|
<td v-if="file.dir">{{ file.include_items }}项</td>
|
||||||
<td v-else>{{file.size | size}}</td>
|
<td v-else>{{file.size | size}}</td>
|
||||||
<td>{{file.updated_at | ts2date}}</td>
|
<td>{{file.updated_at | ts2date}}</td>
|
||||||
<td v-if="!fileSelect.selectShare"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td>
|
<td v-if="!fileSelect.selectShare"><a class="cursor-pointer text-muted" @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td>
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -500,6 +596,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast 提示 -->
|
||||||
|
<div class="toast-container">
|
||||||
|
<div v-for="toast in toasts" :key="toast.id" class="toast show shadow-sm" :class="toast.type" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div class="toast-body d-flex align-items-center">
|
||||||
|
<i class="bi mr-2" :class="getToastIcon(toast.type)"></i>
|
||||||
|
<span>{{ toast.message }}</span>
|
||||||
|
<button type="button" class="ml-auto close" @click="removeToast(toast.id)" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -518,14 +627,21 @@
|
|||||||
tasklist: [],
|
tasklist: [],
|
||||||
magic_regex: {},
|
magic_regex: {},
|
||||||
source: {
|
source: {
|
||||||
|
net: {
|
||||||
|
enable: ""
|
||||||
|
},
|
||||||
cloudsaver: {
|
cloudsaver: {
|
||||||
server: "",
|
server: "",
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
token: ""
|
token: ""
|
||||||
|
},
|
||||||
|
pansou: {
|
||||||
|
server: ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
toasts: [],
|
||||||
newTask: {
|
newTask: {
|
||||||
taskname: "",
|
taskname: "",
|
||||||
shareurl: "",
|
shareurl: "",
|
||||||
@ -555,13 +671,17 @@
|
|||||||
configModified: false,
|
configModified: false,
|
||||||
fileSelect: {
|
fileSelect: {
|
||||||
index: null,
|
index: null,
|
||||||
|
share: {},
|
||||||
shareurl: "",
|
shareurl: "",
|
||||||
stoken: "",
|
stoken: "",
|
||||||
fileList: [],
|
fileList: [],
|
||||||
paths: [],
|
paths: [],
|
||||||
selectDir: true,
|
selectDir: true,
|
||||||
selectShare: true,
|
selectShare: true,
|
||||||
|
switchShare: false,
|
||||||
previewRegex: false,
|
previewRegex: false,
|
||||||
|
sortBy: "updated_at",
|
||||||
|
sortOrder: "desc"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
@ -655,6 +775,16 @@
|
|||||||
token: ""
|
token: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!config_data.source.pansou) {
|
||||||
|
config_data.source.pansou = {
|
||||||
|
server: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!config_data.source.net) {
|
||||||
|
config_data.source.net = {
|
||||||
|
enable: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
this.formData = config_data;
|
this.formData = config_data;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.configModified = false;
|
this.configModified = false;
|
||||||
@ -687,8 +817,10 @@
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
this.configModified = false;
|
this.configModified = false;
|
||||||
|
this.showToast(response.data.message, 'success');
|
||||||
|
} else {
|
||||||
|
this.showToast(response.data.message, 'error');
|
||||||
}
|
}
|
||||||
alert(response.data.message);
|
|
||||||
console.log('Config saved result:', response.data);
|
console.log('Config saved result:', response.data);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@ -909,6 +1041,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
searchSuggestions(index, taskname, deep = 1) {
|
searchSuggestions(index, taskname, deep = 1) {
|
||||||
|
taskname = taskname.replace(/\((19|20)\d{2}\)/g, '').trim();
|
||||||
if (taskname.length < 2) {
|
if (taskname.length < 2) {
|
||||||
console.log(`任务名[${taskname}]过短${taskname.length} 不进行搜索`);
|
console.log(`任务名[${taskname}]过短${taskname.length} 不进行搜索`);
|
||||||
return;
|
return;
|
||||||
@ -938,7 +1071,9 @@
|
|||||||
selectSuggestion(index, suggestion) {
|
selectSuggestion(index, suggestion) {
|
||||||
this.smart_param.showSuggestions = false;
|
this.smart_param.showSuggestions = false;
|
||||||
this.fileSelect.selectDir = true;
|
this.fileSelect.selectDir = true;
|
||||||
|
this.fileSelect.switchShare = true;
|
||||||
this.fileSelect.previewRegex = false;
|
this.fileSelect.previewRegex = false;
|
||||||
|
this.fileSelect.share = suggestion;
|
||||||
this.showShareSelect(index, suggestion.shareurl);
|
this.showShareSelect(index, suggestion.shareurl);
|
||||||
},
|
},
|
||||||
addMagicRegex() {
|
addMagicRegex() {
|
||||||
@ -948,7 +1083,7 @@
|
|||||||
updateMagicRegexKey(oldKey, newKey) {
|
updateMagicRegexKey(oldKey, newKey) {
|
||||||
if (oldKey !== newKey) {
|
if (oldKey !== newKey) {
|
||||||
if (this.formData.magic_regex[newKey]) {
|
if (this.formData.magic_regex[newKey]) {
|
||||||
alert(`魔法名 [${newKey}] 已存在,请使用其他名称`);
|
this.showToast(`魔法名 [${newKey}] 已存在,请使用其他名称`, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]);
|
this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]);
|
||||||
@ -968,7 +1103,7 @@
|
|||||||
if (response.data.code == 0) {
|
if (response.data.code == 0) {
|
||||||
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid != fid);
|
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid != fid);
|
||||||
} else {
|
} else {
|
||||||
alert('删除失败:' + response.data.message);
|
this.showToast('删除失败:' + response.data.message, 'error');
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error /delete_file:', error);
|
console.error('Error /delete_file:', error);
|
||||||
@ -984,7 +1119,8 @@
|
|||||||
axios.get('/get_savepath_detail', {
|
axios.get('/get_savepath_detail', {
|
||||||
params: params
|
params: params
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
this.fileSelect.fileList = response.data.data.list
|
this.fileSelect.fileList = response.data.data.list;
|
||||||
|
this.sortFileList(this.fileSelect.sortBy, this.fileSelect.sortOrder);
|
||||||
if (response.data.data.paths?.length > 0) {
|
if (response.data.data.paths?.length > 0) {
|
||||||
this.fileSelect.paths = response.data.data.paths
|
this.fileSelect.paths = response.data.data.paths
|
||||||
}
|
}
|
||||||
@ -998,6 +1134,7 @@
|
|||||||
showSavepathSelect(index) {
|
showSavepathSelect(index) {
|
||||||
this.fileSelect.selectShare = false;
|
this.fileSelect.selectShare = false;
|
||||||
this.fileSelect.selectDir = true;
|
this.fileSelect.selectDir = true;
|
||||||
|
this.fileSelect.switchShare = false;
|
||||||
this.fileSelect.previewRegex = false;
|
this.fileSelect.previewRegex = false;
|
||||||
this.fileSelect.error = undefined;
|
this.fileSelect.error = undefined;
|
||||||
this.fileSelect.fileList = [];
|
this.fileSelect.fileList = [];
|
||||||
@ -1017,6 +1154,7 @@
|
|||||||
}).then(response => {
|
}).then(response => {
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
this.fileSelect.fileList = response.data.data.list;
|
this.fileSelect.fileList = response.data.data.list;
|
||||||
|
this.sortFileList(this.fileSelect.sortBy, this.fileSelect.sortOrder);
|
||||||
this.fileSelect.paths = response.data.data.paths;
|
this.fileSelect.paths = response.data.data.paths;
|
||||||
this.fileSelect.stoken = response.data.data.stoken;
|
this.fileSelect.stoken = response.data.data.stoken;
|
||||||
} else {
|
} else {
|
||||||
@ -1044,6 +1182,23 @@
|
|||||||
$('#fileSelectModal').modal('toggle');
|
$('#fileSelectModal').modal('toggle');
|
||||||
this.getShareDetail();
|
this.getShareDetail();
|
||||||
},
|
},
|
||||||
|
switchShare(index) {
|
||||||
|
currentIndex = this.smart_param.taskSuggestions.data.indexOf(this.fileSelect.share);
|
||||||
|
nextIndex = currentIndex + index;
|
||||||
|
if (nextIndex < 0) {
|
||||||
|
this.showToast("没有上一个啦", "info");
|
||||||
|
} else if (nextIndex >= this.smart_param.taskSuggestions.data.length) {
|
||||||
|
this.showToast("没有下一个啦", "info");
|
||||||
|
} else {
|
||||||
|
this.fileSelect.error = "";
|
||||||
|
this.fileSelect.stoken = "";
|
||||||
|
this.fileSelect.share = this.smart_param.taskSuggestions.data[nextIndex];
|
||||||
|
this.fileSelect.shareurl = this.smart_param.taskSuggestions.data[nextIndex].shareurl;
|
||||||
|
this.fileSelect.paths = [];
|
||||||
|
this.fileSelect.fileList = [];
|
||||||
|
this.getShareDetail();
|
||||||
|
}
|
||||||
|
},
|
||||||
navigateTo(fid, name) {
|
navigateTo(fid, name) {
|
||||||
dir = { fid: fid, name: name }
|
dir = { fid: fid, name: name }
|
||||||
if (this.fileSelect.selectShare) {
|
if (this.fileSelect.selectShare) {
|
||||||
@ -1085,12 +1240,111 @@
|
|||||||
} else if (shareurl.includes(dir.fid)) {
|
} else if (shareurl.includes(dir.fid)) {
|
||||||
shareurl = shareurl.match(`.*/${dir.fid}[^/]*`)[0]
|
shareurl = shareurl.match(`.*/${dir.fid}[^/]*`)[0]
|
||||||
} else if (shareurl.includes('#/list/share')) {
|
} else if (shareurl.includes('#/list/share')) {
|
||||||
shareurl = `${shareurl}/${dir.fid}-${dir.name?.replace('-', '*101')}`
|
shareurl = `${shareurl.split('#')[0]}#/list/share/${dir.fid}`
|
||||||
} else {
|
} else {
|
||||||
shareurl = `${shareurl}#/list/share/${dir.fid}-${dir.name?.replace('-', '*101')}`
|
shareurl = `${shareurl.split('#')[0]}#/list/share/${dir.fid}`
|
||||||
}
|
}
|
||||||
return shareurl;
|
return shareurl;
|
||||||
|
},
|
||||||
|
sortFileList(column, order) {
|
||||||
|
if (this.fileSelect.sortBy === column && !order) {
|
||||||
|
this.fileSelect.sortOrder = this.fileSelect.sortOrder === "asc" ? "desc" : "asc";
|
||||||
|
} else {
|
||||||
|
this.fileSelect.sortBy = column;
|
||||||
|
this.fileSelect.sortOrder = order || "asc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.fileSelect.fileList.sort((a, b) => {
|
||||||
|
let valA = a[this.fileSelect.sortBy];
|
||||||
|
let valB = b[this.fileSelect.sortBy];
|
||||||
|
|
||||||
|
if (typeof valA === "string") valA = valA.toLowerCase();
|
||||||
|
if (typeof valB === "string") valB = valB.toLowerCase();
|
||||||
|
|
||||||
|
if (valA < valB) return this.fileSelect.sortOrder === "asc" ? -1 : 1;
|
||||||
|
if (valA > valB) return this.fileSelect.sortOrder === "asc" ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
inputRawMagicRegex(task) {
|
||||||
|
const item = this.formData.magic_regex[task.pattern];
|
||||||
|
if (item) {
|
||||||
|
task.pattern = item.pattern;
|
||||||
|
task.replace = item.replace;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyText(text, callback = () => { }) {
|
||||||
|
if (!text) {
|
||||||
|
console.error('No text to copy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
} else {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.top = '0';
|
||||||
|
textarea.style.left = '0';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
textarea.setSelectionRange(0, 99999);
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
copyTaskToClipboard(index) {
|
||||||
|
const task = { ...this.formData.tasklist[index] };
|
||||||
|
delete task.addition;
|
||||||
|
const _this = this;
|
||||||
|
this.copyText(JSON.stringify(task), function () {
|
||||||
|
_this.showToast("任务参数已复制到剪贴板", "success");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async addTaskForClipboard() {
|
||||||
|
text = null
|
||||||
|
try {
|
||||||
|
text = await navigator.clipboard.readText();
|
||||||
|
} catch (error) {
|
||||||
|
text = prompt("当前环境不支持自动读取粘贴板,请手动粘贴任务参数", "");
|
||||||
|
}
|
||||||
|
if (text) {
|
||||||
|
try {
|
||||||
|
let task = JSON.parse(text);
|
||||||
|
task = { ...this.newTask, ...task };
|
||||||
|
this.formData.tasklist.push(task);
|
||||||
|
this.showToast("剪贴板参数已成功导入任务", "success");
|
||||||
|
// 滚到最下
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#collapse_' + (this.formData.tasklist.length - 1)).collapse('show').on('shown.bs.collapse', () => {
|
||||||
|
this.scrollToX();
|
||||||
|
});
|
||||||
|
}, 1);
|
||||||
|
} catch (error) {
|
||||||
|
this.showToast("解析剪贴板内容失败", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showToast(message, type = 'info', duration = 3000) {
|
||||||
|
const id = Date.now();
|
||||||
|
this.toasts.push({ id, message, type });
|
||||||
|
setTimeout(() => {
|
||||||
|
this.removeToast(id);
|
||||||
|
}, duration);
|
||||||
|
},
|
||||||
|
removeToast(id) {
|
||||||
|
this.toasts = this.toasts.filter(t => t.id !== id);
|
||||||
|
},
|
||||||
|
getToastIcon(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'success': return 'bi-check-circle-fill text-success';
|
||||||
|
case 'error': return 'bi-exclamation-circle-fill text-danger';
|
||||||
|
case 'warning': return 'bi-exclamation-triangle-fill text-warning';
|
||||||
|
default: return 'bi-info-circle-fill text-info';
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
13
notify.py
13
notify.py
@ -128,6 +128,10 @@ 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_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||||
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
@ -825,7 +829,14 @@ def ntfy(title: str, content: str) -> None:
|
|||||||
encoded_title = encode_rfc2047(title)
|
encoded_title = encode_rfc2047(title)
|
||||||
|
|
||||||
data = content.encode(encoding="utf-8")
|
data = content.encode(encoding="utf-8")
|
||||||
headers = {"Title": encoded_title, "Priority": priority} # 使用编码后的 title
|
headers = {"Title": encoded_title, "Priority": priority, "Icon": "https://qn.whyour.cn/logo.png"} # 使用编码后的 title
|
||||||
|
if push_config.get("NTFY_TOKEN"):
|
||||||
|
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)
|
||||||
|
|||||||
@ -75,7 +75,7 @@ docker run -d \
|
|||||||
|
|
||||||
```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"
|
||||||
@ -92,3 +92,4 @@ docker run -d \
|
|||||||
| ------- | -------------------- | --------------------------------------- |
|
| ------- | -------------------- | --------------------------------------- |
|
||||||
| 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_strm_gen.py | 自动生成strm | [xiaoQQya](https://github.com/xiaoQQya) |
|
||||||
|
| alist_sync.py | 调用 alist 实现跨网盘转存 | [jenfonro](https://github.com/jenfonro) |
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
[
|
[
|
||||||
|
"smartstrm",
|
||||||
|
"fnv_refresh_v2",
|
||||||
"alist",
|
"alist",
|
||||||
"alist_strm",
|
"alist_strm",
|
||||||
"alist_strm_gen",
|
"alist_strm_gen",
|
||||||
|
"alist_sync",
|
||||||
"aria2",
|
"aria2",
|
||||||
"emby",
|
"emby",
|
||||||
"plex"
|
"plex",
|
||||||
|
"fnv"
|
||||||
]
|
]
|
||||||
313
plugins/alist_sync.py
Normal file
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 []
|
||||||
312
plugins/fnv.py
Normal file
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
BIN
plugins/fnv_refresh_v2.so
Normal file
Binary file not shown.
75
plugins/smartstrm.py
Normal file
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)}")
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# !/usr/bin/env python3
|
# !/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Modify: 2024-11-13
|
# Modify: 2025-09-05
|
||||||
# Repo: https://github.com/Cp0204/quark_auto_save
|
# Repo: https://github.com/Cp0204/quark_auto_save
|
||||||
# ConfigFile: quark_config.json
|
# ConfigFile: quark_config.json
|
||||||
"""
|
"""
|
||||||
@ -18,6 +18,7 @@ import importlib
|
|||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from natsort import natsorted
|
||||||
|
|
||||||
# 兼容青龙
|
# 兼容青龙
|
||||||
try:
|
try:
|
||||||
@ -95,20 +96,26 @@ class Config:
|
|||||||
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "").split(",")
|
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "").split(",")
|
||||||
plugins_available = {}
|
plugins_available = {}
|
||||||
task_plugins_config = {}
|
task_plugins_config = {}
|
||||||
|
# 获取所有模块
|
||||||
|
py_ext = [".py", ".pyd"] if sys.platform == "win32" else [".py", ".so"]
|
||||||
all_modules = [
|
all_modules = [
|
||||||
f.replace(".py", "") for f in os.listdir(plugins_dir) if f.endswith(".py")
|
f.replace(ext, "")
|
||||||
|
for f in os.listdir(plugins_dir)
|
||||||
|
for ext in py_ext
|
||||||
|
if f.endswith(ext)
|
||||||
]
|
]
|
||||||
# 调整模块优先级
|
# 调整模块优先级
|
||||||
priority_path = os.path.join(plugins_dir, "_priority.json")
|
priority_path = os.path.join(plugins_dir, "_priority.json")
|
||||||
try:
|
try:
|
||||||
with open(priority_path, encoding="utf-8") as f:
|
with open(priority_path, encoding="utf-8") as f:
|
||||||
priority_modules = json.load(f)
|
priority_modules = json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
priority_modules = []
|
||||||
if priority_modules:
|
if priority_modules:
|
||||||
all_modules = [
|
all_modules = [
|
||||||
module for module in priority_modules if module in all_modules
|
module for module in priority_modules if module in all_modules
|
||||||
] + [module for module in all_modules if module not in priority_modules]
|
] + [module for module in all_modules if module not in priority_modules]
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
# 加载模块
|
||||||
priority_modules = []
|
|
||||||
for module_name in all_modules:
|
for module_name in all_modules:
|
||||||
if f"-{module_name}" in PLUGIN_FLAGS:
|
if f"-{module_name}" in PLUGIN_FLAGS:
|
||||||
continue
|
continue
|
||||||
@ -127,7 +134,6 @@ class Config:
|
|||||||
task_plugins_config[module_name] = plugin.default_task_config
|
task_plugins_config[module_name] = plugin.default_task_config
|
||||||
except (ImportError, AttributeError) as e:
|
except (ImportError, AttributeError) as e:
|
||||||
print(f"载入模块 {module_name} 失败: {e}")
|
print(f"载入模块 {module_name} 失败: {e}")
|
||||||
print()
|
|
||||||
return plugins_available, plugins_config, task_plugins_config
|
return plugins_available, plugins_config, task_plugins_config
|
||||||
|
|
||||||
def breaking_change_update(config_data):
|
def breaking_change_update(config_data):
|
||||||
@ -194,6 +200,9 @@ class MagicRename:
|
|||||||
"八",
|
"八",
|
||||||
"九",
|
"九",
|
||||||
"十",
|
"十",
|
||||||
|
"百",
|
||||||
|
"千",
|
||||||
|
"万",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, magic_regex={}, magic_variable={}):
|
def __init__(self, magic_regex={}, magic_variable={}):
|
||||||
@ -258,13 +267,14 @@ class MagicRename:
|
|||||||
"""自定义排序键"""
|
"""自定义排序键"""
|
||||||
for i, keyword in enumerate(self.priority_list):
|
for i, keyword in enumerate(self.priority_list):
|
||||||
if keyword in name:
|
if keyword in name:
|
||||||
return name.replace(keyword, f"{i:02d}") # 替换为数字,方便排序
|
name = name.replace(keyword, f"_{i:02d}_") # 替换为数字,方便排序
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def sort_file_list(self, file_list, dir_filename_dict={}):
|
def sort_file_list(self, file_list, dir_filename_dict={}):
|
||||||
"""文件列表统一排序,给{I+}赋值"""
|
"""文件列表统一排序,给{I+}赋值"""
|
||||||
filename_list = [
|
filename_list = [
|
||||||
f["file_name_re"]
|
# 强制加入`文件修改时间`字段供排序,效果:1无可排序字符时则按修改时间排序,2和目录已有文件重名时始终在其后
|
||||||
|
f"{f['file_name_re']}_{f['updated_at']}"
|
||||||
for f in file_list
|
for f in file_list
|
||||||
if f.get("file_name_re") and not f["dir"]
|
if f.get("file_name_re") and not f["dir"]
|
||||||
]
|
]
|
||||||
@ -273,14 +283,22 @@ class MagicRename:
|
|||||||
# print(f"dir_filename_list: {dir_filename_list}")
|
# print(f"dir_filename_list: {dir_filename_list}")
|
||||||
# 合并目录文件列表
|
# 合并目录文件列表
|
||||||
filename_list = list(set(filename_list) | set(dir_filename_dict.values()))
|
filename_list = list(set(filename_list) | set(dir_filename_dict.values()))
|
||||||
filename_list.sort(key=self._custom_sort_key)
|
filename_list = natsorted(filename_list, key=self._custom_sort_key)
|
||||||
# print(f"filename_list_sort: {filename_list}")
|
filename_index = {}
|
||||||
|
for name in filename_list:
|
||||||
|
if name in dir_filename_dict.values():
|
||||||
|
continue
|
||||||
|
i = filename_list.index(name) + 1
|
||||||
|
while i in dir_filename_dict.keys():
|
||||||
|
i += 1
|
||||||
|
dir_filename_dict[i] = name
|
||||||
|
filename_index[name] = i
|
||||||
for file in file_list:
|
for file in file_list:
|
||||||
if file.get("file_name_re"):
|
if file.get("file_name_re"):
|
||||||
if match := re.search(r"\{I+\}", file["file_name_re"]):
|
if match := re.search(r"\{I+\}", file["file_name_re"]):
|
||||||
i = filename_list.index(file["file_name_re"]) + 1
|
i = filename_index.get(
|
||||||
while i in dir_filename_dict.keys():
|
f"{file['file_name_re']}_{file['updated_at']}", 0
|
||||||
i += 1
|
)
|
||||||
file["file_name_re"] = re.sub(
|
file["file_name_re"] = re.sub(
|
||||||
match.group(),
|
match.group(),
|
||||||
str(i).zfill(match.group().count("I")),
|
str(i).zfill(match.group().count("I")),
|
||||||
@ -289,11 +307,11 @@ class MagicRename:
|
|||||||
|
|
||||||
def set_dir_file_list(self, file_list, replace):
|
def set_dir_file_list(self, file_list, replace):
|
||||||
"""设置目录文件列表"""
|
"""设置目录文件列表"""
|
||||||
if not file_list:
|
|
||||||
return
|
|
||||||
self.dir_filename_dict = {}
|
self.dir_filename_dict = {}
|
||||||
filename_list = [f["file_name"] for f in file_list if not f["dir"]]
|
filename_list = [f["file_name"] for f in file_list if not f["dir"]]
|
||||||
filename_list.sort()
|
filename_list.sort()
|
||||||
|
if not filename_list:
|
||||||
|
return
|
||||||
if match := re.search(r"\{I+\}", replace):
|
if match := re.search(r"\{I+\}", replace):
|
||||||
# 由替换式转换匹配式
|
# 由替换式转换匹配式
|
||||||
magic_i = match.group()
|
magic_i = match.group()
|
||||||
@ -326,7 +344,7 @@ class MagicRename:
|
|||||||
if match := re.search(r"\{I+\}", filename):
|
if match := re.search(r"\{I+\}", filename):
|
||||||
magic_i = match.group()
|
magic_i = match.group()
|
||||||
pattern_i = r"\d" * magic_i.count("I")
|
pattern_i = r"\d" * magic_i.count("I")
|
||||||
pattern = filename.replace(magic_i, pattern_i)
|
pattern = re.escape(filename).replace(re.escape(magic_i), pattern_i)
|
||||||
for filename in filename_list:
|
for filename in filename_list:
|
||||||
if re.match(pattern, filename):
|
if re.match(pattern, filename):
|
||||||
return filename
|
return filename
|
||||||
@ -484,7 +502,9 @@ class Quark:
|
|||||||
).json()
|
).json()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_detail(self, pwd_id, stoken, pdir_fid, _fetch_share=0):
|
def get_detail(
|
||||||
|
self, pwd_id, stoken, pdir_fid, _fetch_share=0, fetch_share_full_path=0
|
||||||
|
):
|
||||||
list_merge = []
|
list_merge = []
|
||||||
page = 1
|
page = 1
|
||||||
while True:
|
while True:
|
||||||
@ -502,6 +522,8 @@ class Quark:
|
|||||||
"_fetch_share": _fetch_share,
|
"_fetch_share": _fetch_share,
|
||||||
"_fetch_total": "1",
|
"_fetch_total": "1",
|
||||||
"_sort": "file_type:asc,updated_at:desc",
|
"_sort": "file_type:asc,updated_at:desc",
|
||||||
|
"ver": "2",
|
||||||
|
"fetch_share_full_path": fetch_share_full_path,
|
||||||
}
|
}
|
||||||
response = self._send_request("GET", url, params=querystring).json()
|
response = self._send_request("GET", url, params=querystring).json()
|
||||||
if response["code"] != 0:
|
if response["code"] != 0:
|
||||||
@ -551,6 +573,8 @@ class Quark:
|
|||||||
"_fetch_sub_dirs": "0",
|
"_fetch_sub_dirs": "0",
|
||||||
"_sort": "file_type:asc,updated_at:desc",
|
"_sort": "file_type:asc,updated_at:desc",
|
||||||
"_fetch_full_path": kwargs.get("fetch_full_path", 0),
|
"_fetch_full_path": kwargs.get("fetch_full_path", 0),
|
||||||
|
"fetch_all_file": 1, # 跟随Web端,作用未知
|
||||||
|
"fetch_risk_file_name": 1, # 如无此参数,违规文件名会被变 ***
|
||||||
}
|
}
|
||||||
response = self._send_request("GET", url, params=querystring).json()
|
response = self._send_request("GET", url, params=querystring).json()
|
||||||
if response["code"] != 0:
|
if response["code"] != 0:
|
||||||
@ -603,7 +627,9 @@ class Quark:
|
|||||||
"__t": datetime.now().timestamp(),
|
"__t": datetime.now().timestamp(),
|
||||||
}
|
}
|
||||||
response = self._send_request("GET", url, params=querystring).json()
|
response = self._send_request("GET", url, params=querystring).json()
|
||||||
if response["data"]["status"] != 0:
|
if response["status"] != 200:
|
||||||
|
return response
|
||||||
|
if response["data"]["status"] == 2:
|
||||||
if retry_index > 0:
|
if retry_index > 0:
|
||||||
print()
|
print()
|
||||||
break
|
break
|
||||||
@ -696,6 +722,7 @@ class Quark:
|
|||||||
match_pwd = re.search(r"pwd=(\w+)", url)
|
match_pwd = re.search(r"pwd=(\w+)", url)
|
||||||
passcode = match_pwd.group(1) if match_pwd else ""
|
passcode = match_pwd.group(1) if match_pwd else ""
|
||||||
# path: fid-name
|
# path: fid-name
|
||||||
|
# Legacy 20250905
|
||||||
paths = []
|
paths = []
|
||||||
matches = re.findall(r"/(\w{32})-?([^/]+)?", url)
|
matches = re.findall(r"/(\w{32})-?([^/]+)?", url)
|
||||||
for match in matches:
|
for match in matches:
|
||||||
@ -856,18 +883,21 @@ class Quark:
|
|||||||
mr = MagicRename(CONFIG_DATA.get("magic_regex", {}))
|
mr = MagicRename(CONFIG_DATA.get("magic_regex", {}))
|
||||||
mr.set_taskname(task["taskname"])
|
mr.set_taskname(task["taskname"])
|
||||||
|
|
||||||
|
# 魔法正则转换
|
||||||
|
pattern, replace = mr.magic_regex_conv(
|
||||||
|
task.get("pattern", ""), task.get("replace", "")
|
||||||
|
)
|
||||||
# 需保存的文件清单
|
# 需保存的文件清单
|
||||||
need_save_list = []
|
need_save_list = []
|
||||||
# 添加符合的
|
# 添加符合的
|
||||||
for share_file in share_file_list:
|
for share_file in share_file_list:
|
||||||
if share_file["dir"] and task.get("update_subdir", False):
|
search_pattern = (
|
||||||
pattern, replace = task["update_subdir"], ""
|
task["update_subdir"]
|
||||||
else:
|
if share_file["dir"] and task.get("update_subdir")
|
||||||
pattern, replace = mr.magic_regex_conv(
|
else pattern
|
||||||
task.get("pattern", ""), task.get("replace", "")
|
|
||||||
)
|
)
|
||||||
# 正则文件名匹配
|
# 正则文件名匹配
|
||||||
if re.search(pattern, share_file["file_name"]):
|
if re.search(search_pattern, share_file["file_name"]):
|
||||||
# 判断原文件名是否存在,处理忽略扩展名
|
# 判断原文件名是否存在,处理忽略扩展名
|
||||||
if not mr.is_exists(
|
if not mr.is_exists(
|
||||||
share_file["file_name"],
|
share_file["file_name"],
|
||||||
@ -891,8 +921,35 @@ class Quark:
|
|||||||
need_save_list.append(share_file)
|
need_save_list.append(share_file)
|
||||||
elif share_file["dir"]:
|
elif share_file["dir"]:
|
||||||
# 存在并是一个目录,历遍子目录
|
# 存在并是一个目录,历遍子目录
|
||||||
if task.get("update_subdir", False):
|
if task.get("update_subdir", False) and re.search(
|
||||||
if re.search(task["update_subdir"], share_file["file_name"]):
|
task["update_subdir"], share_file["file_name"]
|
||||||
|
):
|
||||||
|
if task.get("update_subdir_resave_mode", False):
|
||||||
|
# 重存模式:删除该目录下所有文件,重新转存
|
||||||
|
print(f"重存子目录:{savepath}/{share_file['file_name']}")
|
||||||
|
# 删除子目录、回收站中彻底删除
|
||||||
|
subdir = next(
|
||||||
|
(
|
||||||
|
f
|
||||||
|
for f in dir_file_list
|
||||||
|
if f["file_name"] == share_file["file_name"]
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
delete_return = self.delete([subdir["fid"]])
|
||||||
|
self.query_task(delete_return["data"]["task_id"])
|
||||||
|
recycle_list = self.recycle_list()
|
||||||
|
record_id_list = [
|
||||||
|
item["record_id"]
|
||||||
|
for item in recycle_list
|
||||||
|
if item["fid"] == subdir["fid"]
|
||||||
|
]
|
||||||
|
self.recycle_remove(record_id_list)
|
||||||
|
# 作为新文件添加到转存列表
|
||||||
|
share_file["file_name_re"] = share_file["file_name"]
|
||||||
|
need_save_list.append(share_file)
|
||||||
|
else:
|
||||||
|
# 递归模式
|
||||||
print(f"检查子目录:{savepath}/{share_file['file_name']}")
|
print(f"检查子目录:{savepath}/{share_file['file_name']}")
|
||||||
subdir_tree = self.dir_check_and_save(
|
subdir_tree = self.dir_check_and_save(
|
||||||
task,
|
task,
|
||||||
@ -924,15 +981,31 @@ class Quark:
|
|||||||
fid_list = [item["fid"] for item in need_save_list]
|
fid_list = [item["fid"] for item in need_save_list]
|
||||||
fid_token_list = [item["share_fid_token"] for item in need_save_list]
|
fid_token_list = [item["share_fid_token"] for item in need_save_list]
|
||||||
if fid_list:
|
if fid_list:
|
||||||
save_file_return = self.save_file(
|
|
||||||
fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken
|
|
||||||
)
|
|
||||||
err_msg = None
|
err_msg = None
|
||||||
|
save_as_top_fids = []
|
||||||
|
while fid_list:
|
||||||
|
# 分次转存,100个/次,因query_task返回save_as_top_fids最多100
|
||||||
|
save_file_return = self.save_file(
|
||||||
|
fid_list[:100], fid_token_list[:100], to_pdir_fid, pwd_id, stoken
|
||||||
|
)
|
||||||
|
fid_list = fid_list[100:]
|
||||||
|
fid_token_list = fid_token_list[100:]
|
||||||
if save_file_return["code"] == 0:
|
if save_file_return["code"] == 0:
|
||||||
|
# 转存成功,查询转存结果
|
||||||
task_id = save_file_return["data"]["task_id"]
|
task_id = save_file_return["data"]["task_id"]
|
||||||
query_task_return = self.query_task(task_id)
|
query_task_return = self.query_task(task_id)
|
||||||
if query_task_return["code"] == 0:
|
if query_task_return["code"] == 0:
|
||||||
|
save_as_top_fids.extend(
|
||||||
|
query_task_return["data"]["save_as"]["save_as_top_fids"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
err_msg = query_task_return["message"]
|
||||||
|
else:
|
||||||
|
err_msg = save_file_return["message"]
|
||||||
|
if err_msg:
|
||||||
|
add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n")
|
||||||
# 建立目录树
|
# 建立目录树
|
||||||
|
if len(need_save_list) == len(save_as_top_fids):
|
||||||
for index, item in enumerate(need_save_list):
|
for index, item in enumerate(need_save_list):
|
||||||
icon = self._get_file_icon(item)
|
icon = self._get_file_icon(item)
|
||||||
tree.create_node(
|
tree.create_node(
|
||||||
@ -942,18 +1015,12 @@ class Quark:
|
|||||||
data={
|
data={
|
||||||
"file_name": item["file_name"],
|
"file_name": item["file_name"],
|
||||||
"file_name_re": item["file_name_re"],
|
"file_name_re": item["file_name_re"],
|
||||||
"fid": f"{query_task_return['data']['save_as']['save_as_top_fids'][index]}",
|
"fid": f"{save_as_top_fids[index]}",
|
||||||
"path": f"{savepath}/{item['file_name_re']}",
|
"path": f"{savepath}/{item['file_name_re']}",
|
||||||
"is_dir": item["dir"],
|
"is_dir": item["dir"],
|
||||||
"obj_category": item.get("obj_category", ""),
|
"obj_category": item.get("obj_category", ""),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
err_msg = query_task_return["message"]
|
|
||||||
else:
|
|
||||||
err_msg = save_file_return["message"]
|
|
||||||
if err_msg:
|
|
||||||
add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n")
|
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
def do_rename(self, tree, node_id=None):
|
def do_rename(self, tree, node_id=None):
|
||||||
@ -1048,6 +1115,7 @@ def do_save(account, tasklist=[]):
|
|||||||
plugins, CONFIG_DATA["plugins"], task_plugins_config = Config.load_plugins(
|
plugins, CONFIG_DATA["plugins"], task_plugins_config = Config.load_plugins(
|
||||||
CONFIG_DATA.get("plugins", {})
|
CONFIG_DATA.get("plugins", {})
|
||||||
)
|
)
|
||||||
|
print()
|
||||||
print(f"转存账号: {account.nickname}")
|
print(f"转存账号: {account.nickname}")
|
||||||
# 获取全部保存目录fid
|
# 获取全部保存目录fid
|
||||||
account.update_savepath_fid(tasklist)
|
account.update_savepath_fid(tasklist)
|
||||||
@ -1115,6 +1183,13 @@ def do_save(account, tasklist=[]):
|
|||||||
plugin.run(task, account=account, tree=is_new_tree) or task
|
plugin.run(task, account=account, tree=is_new_tree) or task
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
|
print(f"===============插件收尾===============")
|
||||||
|
for plugin_name, plugin in plugins.items():
|
||||||
|
if plugin.is_active and hasattr(plugin, "task_after"):
|
||||||
|
data = plugin.task_after()
|
||||||
|
if data.get("config"):
|
||||||
|
CONFIG_DATA["plugins"][plugin_name] = data["config"]
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"magic_regex": {
|
"magic_regex": {
|
||||||
"$TV": {
|
"$TV_REGEX": {
|
||||||
"pattern": ".*?([Ss]\\d{1,2})?(?:[第EePpXx\\.\\-\\_\\( ]{1,2}|^)(\\d{1,3})(?!\\d).*?\\.(mp4|mkv)",
|
"pattern": ".*?([Ss]\\d{1,2})?(?:[第EePpXx\\.\\-\\_\\( ]{1,2}|^)(\\d{1,3})(?!\\d).*?\\.(mp4|mkv)",
|
||||||
"replace": "\\1E\\2.\\3"
|
"replace": "\\1E\\2.\\3"
|
||||||
},
|
},
|
||||||
@ -21,12 +21,12 @@
|
|||||||
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*",
|
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*超前企划)(?!.*训练室)(?!.*蒸蒸日上).*",
|
||||||
"replace": ""
|
"replace": ""
|
||||||
},
|
},
|
||||||
"$SHOW_PRO": {
|
"$SHOW_MAGIC": {
|
||||||
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*",
|
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*",
|
||||||
"replace": "{II}.{TASKNAME}.{DATE}.第{E}期{PART}.{EXT}"
|
"replace": "{TASKNAME}.{SXX}E{II}.第{E}期{PART}.{EXT}"
|
||||||
},
|
},
|
||||||
"$TV_PRO": {
|
"$TV_MAGIC": {
|
||||||
"pattern": "",
|
"pattern": ".*\\.(mp4|mkv|mov|m4v|avi|mpeg|ts)$",
|
||||||
"replace": "{TASKNAME}.{SXX}E{E}.{EXT}"
|
"replace": "{TASKNAME}.{SXX}E{E}.{EXT}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -35,7 +35,7 @@
|
|||||||
"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",
|
||||||
"update_subdir": "4k|1080p"
|
"update_subdir": "4k|1080p"
|
||||||
|
|||||||
@ -2,3 +2,4 @@ flask
|
|||||||
apscheduler
|
apscheduler
|
||||||
requests
|
requests
|
||||||
treelib
|
treelib
|
||||||
|
natsort
|
||||||
Loading…
Reference in New Issue
Block a user