Compare commits

...

33 Commits
v0.7.4 ... main

Author SHA1 Message Date
Cp0204
579c35fadc feat(plugins): 新增 飞牛影视刷新v2 插件并调整优先级
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2026-01-07 23:22:34 +08:00
Cp0204
72506d6b5f feat(plugins): 支持插件 task_after 方法,并可更新自身配置 2026-01-07 22:20:55 +08:00
Cp0204
f3a6d665cf refactor(plugins): 支持编译插件的加载 2026-01-07 19:44:56 +08:00
Cp0204
41201653f1 fix: 修复浏览目录时违规文件名变 X*** 2026-01-07 17:28:03 +08:00
Cp0204
39cac1bacb fix: 移除尝试终止超时子进程的冗余代码块
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2026-01-05 16:04:58 +08:00
Cp0204
55e338f35c fix: 修复任务导入参数缺失导致的错误
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-12-28 12:23:20 +08:00
Cp0204
f7fe5d68e7 feat: 导入任务后自动展开,提升体验
Some checks are pending
Docker Publish / build-and-push (push) Waiting to run
2025-12-28 02:49:15 +08:00
Cp0204
6fc0915117 fix: 剪贴板导入任务环境兼容,不支持读取时提供手动粘贴框 2025-12-28 02:49:15 +08:00
Cp0204
000618ac5e feat: 添加 toast 通知替代 alert 提示 2025-12-28 01:30:25 +08:00
Cp0204
66f39ea9e2 feat(ui): 添加任务分享和剪贴板导入功能 2025-12-28 01:30:25 +08:00
xiaoQQya
ef5c6e4644
feat: 任务保存规则双击魔法匹配可释放填入原始表达式 (#136)
* perf: 任务保存规则支持以魔法匹配为模板调整正则表达式

* feat(ui): 调整魔法正则表达式交互逻辑

- 将 `@change` 事件调整为 `@dblclick`
- 添加 `title` 提示用户“双击可将魔法匹配释放为填入原始正则表达式”

---------

Co-authored-by: Cp0204 <Cp0204@qq.com>
2025-12-28 00:10:56 +08:00
xiaoQQya
9fe3863c31
fix: 修复任务没有新的转存记录时报错的问题 (#135) 2025-12-27 23:27:08 +08:00
Cp0204
7679bbab38 🐛 修复容量限制 (capacity limit) 时的无报错无限转圈
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-12-10 18:40:00 +08:00
Cp0204
365f3de136 🔧 更新 $TV_MAGIC 匹配常见视频格式 2025-12-10 15:20:24 +08:00
tellbin
dbc965c6fe
飞牛插件添加媒体库文件夹路径列表支持,优化刷新指令输出 (#131)
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
Co-authored-by: changguobin <changguobin@kostech.com.cn>
2025-10-30 13:21:08 +08:00
Cp0204
75ccf228cd 📝 更新功能描述与生态项目
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-10-28 19:56:36 +08:00
Cp0204
e2a6238ab9 🔧 调整默认综艺魔法命名配置 2025-10-28 19:53:25 +08:00
Cp0204
98e53b38db QAS一键推送助手:优化错误提示 #127
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
- 设置按钮改为利用 '.pc-member-entrance'
- 增强任务推送接口的错误提示
2025-10-16 12:49:51 +08:00
Cp0204
846bf0345a 🔧 增强代码可读性与优化日志
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-10-14 14:12:30 +08:00
ypq123456789
95ddc95c79
🐛 补充修复:添加 APScheduler 调度器参数,彻底解决任务堆积问题 (#126) 2025-10-14 13:31:02 +08:00
ypq123456789
956105c16e
🐛 修复定时任务调度器卡死导致后续任务无法执行的问题 (#125)
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-10-10 22:41:05 +08:00
Cp0204
3b9ee5eb96 📝 添加 QAS 生态项目推荐
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-09-15 12:26:47 +08:00
Cp0204
a03b57cbb0 🐛 修复 {II} 时反复存相同的内容 #123 2025-09-15 11:14:06 +08:00
Cp0204
2c2aa50a88 🐛 修复一次性转存>100个时的报错
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-09-06 22:17:11 +08:00
Cp0204
5cc955f590 适配官方新的分享子目录链接格式
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-09-05 16:18:30 +08:00
Cp0204
33215957bf 📦 优化版本信息设置和获取方式 2025-09-05 12:18:06 +08:00
Cp0204
473ac0d468 优化 SmartStrm 插件初始化逻辑 2025-09-05 12:17:30 +08:00
Cp0204
0f6b6839c4 🐛 修复切换分享链接时闪现的问题 #117
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-09-03 15:46:58 +08:00
xiaoQQya
e67d95a82b
资源搜索功能优化 (#117)
* perf: 优化资源发布时间解析逻辑
* perf: PanSou 源支持前端深度搜索
* feat: 网络公开搜索源支持启用或关闭
* feat: 文件选择窗口支持切换分享链接
* perf: 优化文件选择窗口资源简介展示
* perf: 优化文件选择窗口资源信息样式
* fix: 修复 net.enable=None 时 lower() 报错
* style: 优化资源简介和切换样式
* style: 优化资源搜索配置样式
---------
Co-authored-by: Cp0204 <Cp0204@qq.com>
2025-09-03 14:37:06 +08:00
Cp0204
edbc4c50c9 📝 更新文档说明
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
2025-08-26 22:58:53 +08:00
Cp0204
119bd3a516 🔧 优化 SmartStrm 插件的错误处理和提示 2025-08-26 19:11:08 +08:00
xiaoQQya
1fad4d7137
🐛 修复资源时间格式解析错误导致搜索失败的问题 (#115)
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
* fix: 修复资源时间格式解析错误导致搜索失败的问题
* feat: 资源搜索结果显示来源通道
2025-08-23 16:26:11 +08:00
xiaoQQya
6f9b009194
🐛 修复资源发布时间时区错误问题 (#114)
Some checks are pending
Docker Publish / build-and-push (push) Waiting to run
2025-08-22 21:08:50 +08:00
15 changed files with 652 additions and 173 deletions

View File

@ -1,6 +1,12 @@
# 使用官方 Python 镜像作为基础镜像
FROM python:3.13-alpine
#构建版本
ARG BUILD_SHA
ARG BUILD_TAG
ENV BUILD_SHA=$BUILD_SHA
ENV BUILD_TAG=$BUILD_TAG
# 设置工作目录
WORKDIR /app
@ -8,17 +14,12 @@ WORKDIR /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"
#构建版本
ARG BUILD_SHA
ARG BUILD_TAG
ENV BUILD_SHA=$BUILD_SHA
ENV BUILD_TAG=$BUILD_TAG
# 端口
EXPOSE 5005

View File

@ -8,7 +8,7 @@
对于一些持续更新的资源,隔段时间去转存十分麻烦。
定期执行本脚本自动转存、文件名整理,配合 OpenList, SmartStrm, 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]
@ -29,7 +29,7 @@
> ⛔️⛔️⛔️ 注意!资源不会每时每刻更新,**严禁设定过高的定时运行频率!** 以免账号风控和给夸克服务器造成不必要的压力。雪山崩塌,每一片雪花都有责任!
> [!NOTE]
> 开发者≠客服,开源免费≠帮你解决使用问题;本项目Wiki和已经相对完善,遇到问题请先翻阅 Issues 和 Wiki ,请勿盲目发问。
> 开发者≠客服,开源免费≠帮你解决使用问题;本项目 Wiki 已经相对完善,遇到问题请先翻阅 Issues 和 Wiki ,请勿盲目发问。
## 功能
@ -58,7 +58,7 @@
- 媒体库整合
- [x] 根据任务名搜索 Emby 媒体库
- [x] 追更或整理后自动刷新 Emby 媒体库
- [x] 媒体库模块化,用户可很方便地[开发自己的媒体库hook模块](./plugins)
- [x] 插件模块化,允许自行开发和挂载[插件](./plugins)
- 其它
- [x] 每日签到领空间 <sup>[?](https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间)</sup>
@ -69,7 +69,7 @@
### Docker 部署
Docker 部署提供 WebUI 管理配置,图形化配置已能满足绝大多数需求。部署命令:
Docker 部署提供 WebUI 进行管理配置,部署命令:
```shell
docker run -d \
@ -107,11 +107,13 @@ services:
管理地址http://yourhost:5005
| 环境变量 | 默认 | 备注 |
| ---------------- | ---------- | -------- |
| `WEBUI_USERNAME` | `admin` | 管理账号 |
| `WEBUI_PASSWORD` | `admin123` | 管理密码 |
| 环境变量 | 默认 | 备注 |
| ---------------- | ---------- | ---------------------------------------- |
| `WEBUI_USERNAME` | `admin` | 管理账号 |
| `WEBUI_PASSWORD` | `admin123` | 管理密码 |
| `PORT` | `5005` | 管理后台端口 |
| `PLUGIN_FLAGS` | | 插件标志,如 `-emby,-aria2` 禁用某些插件 |
| `TASK_TIMEOUT` | `1800` | 任务执行超时时间(秒),超时则任务结束 |
#### 一键更新
@ -128,23 +130,17 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
</details>
### 青龙部署
程序也支持以青龙定时任务的方式运行,但该方式无法使用 WebUI 管理任务,需手动修改配置文件。
青龙部署说明已转移到 Wiki [青龙部署教程](https://github.com/Cp0204/quark-auto-save/wiki/部署教程#青龙部署)
## 使用说明
### 正则处理示例
| pattern | replace | 效果 |
| -------------------------------------- | ------------ | ---------------------------------------------------------------------- |
| `.*` | | 无脑转存所有文件,不整理 |
| `\.mp4$` | | 转存所有 `.mp4` 后缀的文件 |
| `^【电影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 |
| `$TV` | | [魔法匹配](#魔法匹配)剧集文件 |
| pattern | replace | 效果 |
| -------------------------------------- | ----------------------- | ---------------------------------------------------------------------- |
| `.*` | | 无脑转存所有文件,不整理 |
| `\.mp4$` | | 转存所有 `.mp4` 后缀的文件 |
| `^【电影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 |
| `$TV` | | [魔法匹配](#魔法匹配)剧集文件 |
| `^(\d+)\.mp4` | `{TASKNAME}.S02E\1.mp4` | 01.mp4 → 任务名.S02E01.mp4 |
更多正则使用说明:[正则处理教程](https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程)
@ -167,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/使用技巧集锦)
## 生态项目
以下展示 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块钱让我知道开源有价值。谢谢

View File

@ -23,6 +23,7 @@ import subprocess
import requests
import hashlib
import logging
import traceback
import base64
import sys
import os
@ -32,10 +33,30 @@ parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, parent_dir)
from quark_auto_save import Quark, Config, MagicRename
print(
r"""
____ ___ _____
/ __ \ / | / ___/
/ / / / / /| | \__ \
/ /_/ / / ___ |___/ /
\___\_\/_/ |_/____/
-- Quark-Auto-Save --
"""
)
sys.stdout.flush()
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":
return BUILD_TAG
elif BUILD_SHA:
@ -52,6 +73,7 @@ PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
HOST = os.environ.get("HOST", "0.0.0.0")
PORT = os.environ.get("PORT", 5005)
TASK_TIMEOUT = int(os.environ.get("TASK_TIMEOUT", 1800))
config_data = {}
task_plugins_config_default = {}
@ -75,6 +97,8 @@ logging.basicConfig(
# 过滤werkzeug日志输出
if not DEBUG:
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):
@ -235,15 +259,18 @@ def get_task_suggestions():
return jsonify({"success": False, "message": "未登录"})
query = request.args.get("q", "").lower()
deep = request.args.get("d", "").lower()
net_data = config_data.get("source", {}).get("net", {})
cs_data = config_data.get("source", {}).get("cloudsaver", {})
ps_data = config_data.get("source", {}).get("pansou", {})
def net_search():
base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
url = f"{base_url}/task_suggestions?q={query}&d={deep}"
response = requests.get(url)
return response.json()
if str(net_data.get("enable", "true")).lower() != "false":
base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
url = f"{base_url}/task_suggestions?q={query}&d={deep}"
response = requests.get(url)
return response.json()
return []
def cs_search():
if (
cs_data.get("server")
@ -264,13 +291,13 @@ def get_task_suggestions():
search_results = cs.clean_search_results(search.get("data"))
return search_results
return []
def ps_search():
if (ps_data.get("server")):
if ps_data.get("server"):
ps = PanSou(ps_data.get("server"))
return ps.search(query)
return ps.search(query, deep == "1")
return []
try:
search_results = []
with ThreadPoolExecutor(max_workers=3) as executor:
@ -281,17 +308,17 @@ def get_task_suggestions():
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)
search_results.sort(key=lambda x: x.get("datetime", ""), reverse=True)
for item in search_results:
url = item.get("shareurl", "")
if url != "" and url not in link_array:
link_array.append(url)
results.append(item)
return jsonify({"success": True, "data": results})
except Exception as e:
return jsonify({"success": True, "message": f"error: {str(e)}"})
@ -313,7 +340,9 @@ def get_share_detail():
return jsonify(
{"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:
return jsonify(
@ -321,7 +350,10 @@ def get_share_detail():
)
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
# 正则处理预览
@ -455,7 +487,36 @@ def add_task():
# 定时任务执行的函数
def run_python(args):
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 函数执行完成")
# 重新加载任务
@ -471,6 +532,10 @@ def reload_tasks():
trigger=trigger,
args=[f"{SCRIPT_PATH} {CONFIG_PATH}"],
id=SCRIPT_PATH,
max_instances=1, # 最多允许1个实例运行
coalesce=True, # 合并错过的任务,避免堆积
misfire_grace_time=300, # 错过任务的宽限期(秒),超过则跳过
replace_existing=True, # 替换已存在的同ID任务
)
if scheduler.state == 0:
scheduler.start()
@ -489,7 +554,7 @@ def reload_tasks():
def init():
global config_data, task_plugins_config_default
logging.info(f">>> 初始化配置")
logging.info(">>> 初始化配置")
# 检查配置文件是否存在
if not os.path.exists(CONFIG_PATH):
if not os.path.exists(os.path.dirname(CONFIG_PATH)):
@ -527,6 +592,8 @@ def init():
if __name__ == "__main__":
init()
reload_tasks()
logging.info(">>> 启动Web服务")
logging.info(f"运行在: http://{HOST}:{PORT}")
app.run(
debug=DEBUG,
host=HOST,

View File

@ -1,6 +1,6 @@
import datetime
import re
import requests
from sdk.common import iso_to_cst
class CloudSaver:
@ -128,7 +128,7 @@ class CloudSaver:
# 统一发布时间格式
pubdate = item.get("pubDate", "")
if pubdate:
pubdate = datetime.datetime.strptime(pubdate, "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%d %H:%M:%S")
pubdate = iso_to_cst(pubdate)
# 链接去重
if link.get("link") not in link_array:
link_array.append(link.get("link"))
@ -139,8 +139,7 @@ class CloudSaver:
"content": content,
"datetime": pubdate,
"tags": item.get("tags", []),
"channel": item.get("channel", ""),
"channel_id": item.get("channelId", ""),
"channel": item.get("channelId", ""),
"source": "CloudSaver"
}
)

16
app/sdk/common.py Normal file
View 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 ""

View File

@ -1,8 +1,9 @@
import re
import datetime
import requests
from sdk.common import iso_to_cst
class PanSou:
"""
@ -13,7 +14,7 @@ class PanSou:
self.server = server
self.session = requests.Session()
def search(self, keyword: str) -> list:
def search(self, keyword: str, refresh: bool = False) -> list:
"""搜索资源
Args:
@ -24,7 +25,7 @@ class PanSou:
"""
try:
url = f"{self.server.rstrip('/')}/api/search"
params = {"kw": keyword, "cloud_types": ["quark"], "res": "merge", "refresh": True}
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:
@ -55,12 +56,12 @@ class PanSou:
)
format_results = []
link_array = []
for channel in search_results:
url = channel.get("url", "")
note = channel.get("note", "")
tm = channel.get("datetime", "")
for item in search_results:
url = item.get("url", "")
note = item.get("note", "")
tm = item.get("datetime", "")
if tm:
tm = datetime.datetime.strptime(tm, "%Y-%m-%dT%H:%M:%SZ").strftime("%Y-%m-%d %H:%M:%S")
tm = iso_to_cst(tm)
match = re.search(pattern, note)
if match:
@ -73,10 +74,11 @@ class PanSou:
if url != "" and url not in link_array:
link_array.append(url)
format_results.append({
"shareurl": url,
"taskname": title,
"content": content,
"shareurl": url,
"datetime": tm,
"channel": item.get("source", ""),
"source": "PanSou"
})

View File

@ -45,7 +45,7 @@ body {
margin-bottom: 10px;
}
table.jsoneditor-tree > tbody > tr.jsoneditor-expandable:first-child {
table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
display: none;
}
@ -196,3 +196,48 @@ table.jsoneditor-tree > tbody > tr.jsoneditor-expandable:first-child {
max-width: 100%;
height: auto;
}
/* Toast */
.toast-container {
position: fixed;
top: 80px;
right: 20px;
z-index: 9999;
width: 300px;
}
.toast {
background-color: rgba(255, 255, 255, 0.95);
border-radius: 8px;
border-width: 0 0 0 5px !important;
margin-bottom: 10px;
animation: slideIn 0.3s ease forwards;
}
.toast.success {
border-left-color: #28a745 !important;
}
.toast.error {
border-left-color: #dc3545 !important;
}
.toast.warning {
border-left-color: #ffc107 !important;
}
.toast.info {
border-left-color: #17a2b8 !important;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}

View File

@ -2,7 +2,7 @@
// @name QAS一键推送助手
// @namespace https://github.com/Cp0204/quark-auto-save
// @license AGPL
// @version 0.5
// @version 0.6
// @description 在夸克网盘分享页面添加推送到 QAS 的按钮
// @icon https://pan.quark.cn/favicon.ico
// @author Cp0204
@ -76,16 +76,16 @@
}
}
waitForElement('.DetailLayout--client-download--FpyCkdW.ant-dropdown-trigger', (clientDownloadButton) => {
waitForElement('.pc-member-entrance', (PcMemberButton) => {
const qasSettingButton = document.createElement('div');
qasSettingButton.className = 'DetailLayout--client-download--FpyCkdW ant-dropdown-trigger';
qasSettingButton.className = 'pc-member-entrance';
qasSettingButton.innerHTML = 'QAS设置';
qasSettingButton.addEventListener('click', () => {
showQASSettingDialog();
});
clientDownloadButton.parentNode.insertBefore(qasSettingButton, clientDownloadButton.nextSibling);
PcMemberButton.parentNode.insertBefore(qasSettingButton, PcMemberButton.nextSibling);
});
}
@ -155,6 +155,63 @@
},
data: JSON.stringify(data),
onload: function (response) {
// 检查 HTTP 状态码
if (response.status === 401) {
Swal.fire({
title: '认证失败',
text: 'Token 无效或已过期,请重新配置 QAS Token',
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
return;
}
if (response.status === 503) {
Swal.fire({
title: '服务器不可用',
html: `服务器暂时无法处理请求 (503)<br><br>
<small>可能原因<br>
QAS 服务未运行<br>
服务器过载<br>
网络连接问题</small>`,
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
return;
}
// 检查响应内容类型
const contentType = response.responseHeaders.match(/content-type:\s*([^;\s]+)/i);
if (contentType && !contentType[1].includes('application/json')) {
Swal.fire({
title: '认证失败',
html: `服务器返回了非 JSON 响应,可能是 Token 错误<br><br>
<small>响应类型: ${contentType[1]}</small><br>
<small>响应状态: ${response.status}</small>`,
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
return;
}
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.success) {
@ -177,16 +234,34 @@
} catch (e) {
Swal.fire({
title: '解析响应失败',
text: `无法解析 JSON 响应: ${response.responseText}`,
icon: 'error'
html: `<small>
响应状态: ${response.status}<br>
响应内容: ${response.responseText.substring(0, 200)}...<br><br>
错误详情: ${e.message}
</small>`,
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
}
},
onerror: function (error) {
Swal.fire({
title: '任务创建失败',
text: error,
icon: 'error'
title: '网络请求失败',
text: '无法连接到 QAS 服务器,请检查网络连接和服务器地址',
icon: 'error',
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
});
}
});

View File

@ -198,43 +198,70 @@
<div class="row title" title="资源搜索服务配置,用于任务名称智能搜索">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-search"></i> CloudSaver</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源" target="_blank">?</a>
</span>
<h2 style="display: inline-block;"><i class="bi bi-search"></i> 资源搜索</h2>
</div>
</div>
<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.cloudsaver.server" class="form-control" placeholder="资源搜索服务器地址,如 http://172.17.0.1:8008">
<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="form-group row">
<label class="col-sm-2 col-form-label">用户名</label>
<div class="col-sm-10">
<input type="text" v-model="formData.source.cloudsaver.username" class="form-control" placeholder="用户名">
<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">
<label class="col-sm-2 col-form-label">密码</label>
<div class="col-sm-10">
<input type="password" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
<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">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源" target="_blank">?</a>
</span>
</div>
</div>
</div>
<div class="row title" title="资源搜索服务配置,用于任务名称智能搜索">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-search"></i> PanSou</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/fish2018/pansou" target="_blank">?</a>
</span>
<div class="collapse show ml-3" id="collapse_cloudsaver">
<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.cloudsaver.server" class="form-control" placeholder="资源搜索服务器地址,如 http://172.17.0.1:8008">
</div>
</div>
<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.cloudsaver.username" class="form-control" placeholder="用户名">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">密码</label>
<div class="col-sm-10">
<input type="password" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
</div>
</div>
</div>
<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 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>
@ -283,9 +310,10 @@
</div>
</div>
<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" @click="runScriptNow(index)" title="运行此任务" v-else><i class="bi bi-play-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="copyTaskToClipboard(index)" title="复制任务参数到粘贴板"><i class=" bi bi-clipboard-check-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-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 class="collapse ml-3" :id="'collapse_'+index">
@ -303,6 +331,7 @@
<a :href="suggestion.shareurl" target="_blank" @click.stop>{{ suggestion.shareurl }}</a>
</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>
@ -324,7 +353,7 @@
<div class="input-group">
<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">
<button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;fileSelect.sortBy='file_name';fileSelect.sortOrder='desc';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">
<a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a>
</div>
@ -349,9 +378,9 @@
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.previewRegex=true;fileSelect.sortBy='file_name';fileSelect.sortOrder='asc';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>
<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="替换表达式">
<div class="input-group-append" title="保存时只比较文件名的部分01.mp4 和 01.mkv 视同为同一文件,不重复转存">
<div class="input-group-text">
@ -370,7 +399,7 @@
<div class="input-group">
<input type="text" class="form-control" placeholder="可选,只转存修改日期>此文件的文件" name="startfid[]" v-model="task.startfid">
<div class="input-group-append" v-if="task.shareurl">
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;fileSelect.sortBy='updated_at';fileSelect.sortOrder='desc';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>
@ -418,7 +447,10 @@
</div>
<div class="row mt-5">
<div class="col-sm-12 text-center">
<button type="button" class="btn btn-primary" @click="addTask()"><i class="bi bi-plus"></i> 增加任务</button>
<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="addTaskForClipboard()" title="从粘贴板导入"><i class="bi bi-clipboard-plus"></i></button>
</div>
</div>
</div>
</div>
@ -467,6 +499,34 @@
</button>
</div>
<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 v-else>
<!-- 正则处理表达式 -->
@ -536,6 +596,19 @@
</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">&times;</span>
</button>
</div>
</div>
</div>
</div>
@ -554,6 +627,9 @@
tasklist: [],
magic_regex: {},
source: {
net: {
enable: ""
},
cloudsaver: {
server: "",
username: "",
@ -565,6 +641,7 @@
}
},
},
toasts: [],
newTask: {
taskname: "",
shareurl: "",
@ -594,12 +671,14 @@
configModified: false,
fileSelect: {
index: null,
share: {},
shareurl: "",
stoken: "",
fileList: [],
paths: [],
selectDir: true,
selectShare: true,
switchShare: false,
previewRegex: false,
sortBy: "updated_at",
sortOrder: "desc"
@ -701,6 +780,11 @@
server: ""
};
}
if (!config_data.source.net) {
config_data.source.net = {
enable: ""
};
}
this.formData = config_data;
setTimeout(() => {
this.configModified = false;
@ -733,8 +817,10 @@
.then(response => {
if (response.data.success) {
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);
})
.catch(error => {
@ -985,7 +1071,9 @@
selectSuggestion(index, suggestion) {
this.smart_param.showSuggestions = false;
this.fileSelect.selectDir = true;
this.fileSelect.switchShare = true;
this.fileSelect.previewRegex = false;
this.fileSelect.share = suggestion;
this.showShareSelect(index, suggestion.shareurl);
},
addMagicRegex() {
@ -995,7 +1083,7 @@
updateMagicRegexKey(oldKey, newKey) {
if (oldKey !== newKey) {
if (this.formData.magic_regex[newKey]) {
alert(`魔法名 [${newKey}] 已存在,请使用其他名称`);
this.showToast(`魔法名 [${newKey}] 已存在,请使用其他名称`, 'warning');
return;
}
this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]);
@ -1015,7 +1103,7 @@
if (response.data.code == 0) {
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid != fid);
} else {
alert('删除失败:' + response.data.message);
this.showToast('删除失败:' + response.data.message, 'error');
}
}).catch(error => {
console.error('Error /delete_file:', error);
@ -1046,6 +1134,7 @@
showSavepathSelect(index) {
this.fileSelect.selectShare = false;
this.fileSelect.selectDir = true;
this.fileSelect.switchShare = false;
this.fileSelect.previewRegex = false;
this.fileSelect.error = undefined;
this.fileSelect.fileList = [];
@ -1093,6 +1182,23 @@
$('#fileSelectModal').modal('toggle');
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) {
dir = { fid: fid, name: name }
if (this.fileSelect.selectShare) {
@ -1134,9 +1240,9 @@
} else if (shareurl.includes(dir.fid)) {
shareurl = shareurl.match(`.*/${dir.fid}[^/]*`)[0]
} else if (shareurl.includes('#/list/share')) {
shareurl = `${shareurl}/${dir.fid}-${dir.name?.replace('-', '*101')}`
shareurl = `${shareurl.split('#')[0]}#/list/share/${dir.fid}`
} else {
shareurl = `${shareurl}#/list/share/${dir.fid}-${dir.name?.replace('-', '*101')}`
shareurl = `${shareurl.split('#')[0]}#/list/share/${dir.fid}`
}
return shareurl;
},
@ -1159,7 +1265,86 @@
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>

View File

@ -1,10 +1,12 @@
[
"alist",
"smartstrm",
"fnv_refresh_v2",
"alist",
"alist_strm",
"alist_strm_gen",
"alist_sync",
"aria2",
"emby",
"plex"
"plex",
"fnv"
]

View File

@ -25,6 +25,7 @@ class Fnv:
default_task_config = {
"auto_refresh": False, # 是否自动刷新媒体库
"mdb_name": "", # 飞牛影视目标媒体库名称
"mdb_dir_list": "", # 飞牛影视目标媒体库文件夹路径列表,多个用逗号分隔
}
# 定义一个可选键的集合
@ -84,13 +85,17 @@ class Fnv:
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)
self._refresh_library(library_id, dir_list=dir_list)
# =====================================================================
# Internal Methods (内部实现方法)
@ -131,7 +136,8 @@ class Fnv:
try:
response = self.session.request(
method, url, headers=headers, params=params, json=data if data is not None else {}
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()
@ -206,7 +212,7 @@ class Fnv:
print(f"飞牛影视: 未在媒体库列表中找到名为 '{library_name}' 的媒体库 ❌")
return None
def _refresh_library(self, library_id: str) -> bool:
def _refresh_library(self, library_id: str, dir_list: list[str] = None) -> bool:
"""
根据给定的媒体库ID触发一次媒体库扫描/刷新
"""
@ -214,9 +220,13 @@ class Fnv:
print("飞牛影视: 必须先登录才能刷新媒体库。")
return False
print(f"飞牛影视: 正在为媒体库 {library_id} 发送刷新指令...")
if dir_list:
print(f"飞牛影视: 正在为媒体库 {library_id} 发送部分目录{dir_list}刷新指令...")
else:
print(f"飞牛影视: 正在为媒体库 {library_id} 发送刷新指令...")
rel_url = self.API_MDB_SCAN.format(library_id)
response_json = self._make_request('post', rel_url, data={})
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
@ -263,8 +273,10 @@ class Fnv:
nonce = str(random.randint(100000, 999999))
timestamp = str(int(time.time() * 1000))
if method.lower() == 'get' and params:
serialized_str = urlencode(sorted(params.items()))
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)
@ -292,7 +304,7 @@ class Fnv:
将请求体数据序列化为紧凑的JSON字符串
"""
if isinstance(data, dict):
return json.dumps(data, sort_keys=True, separators=(',', ':'))
return json.dumps(data, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
if isinstance(data, str):
return data
if not data:

BIN
plugins/fnv_refresh_v2.so Normal file

Binary file not shown.

View File

@ -7,7 +7,6 @@ class Smartstrm:
"strmtask": "", # SmartStrm 任务名,支持多个如 `tv,movie`
"xlist_path_fix": "", # 路径映射, SmartStrm 任务使用 quark 驱动时无须填写;使用 openlist 驱动时需填写 `/storage_mount_path:/quark_root_dir` ,例如把夸克根目录挂载在 OpenList 的 /quark 下,则填写 `/quark:/` ;以及 SmartStrm 会使 OpenList 强制刷新目录,无需再用 alist 插件刷新。
}
default_task_config = {}
is_active = False
def __init__(self, **kwargs):
@ -19,8 +18,26 @@ class Smartstrm:
else:
print(f"{self.plugin_name} 模块缺少必要参数: {key}")
if self.webhook and self.strmtask:
print(f"SmartStrm 触发任务: {self.strmtask} ")
self.is_active = True
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):
"""
@ -47,16 +64,12 @@ class Smartstrm:
json=payload,
timeout=5,
)
# 检查响应状态
if response.status_code == 200:
response = response.json()
if response.get("success"):
print(
f"SmartStrm 触发任务: [{response['task']['name']}] {response['task']['storage_path']} 成功✅"
)
else:
print(f"SmartStrm 触发任务: {response['message']}")
response = response.json()
if response.get("success"):
print(
f"SmartStrm 触发任务: [{response['task']['name']}] {response['task']['storage_path']} 成功✅"
)
else:
print(f"SmartStrm 触发任务: {response.status_code}")
print(f"SmartStrm 触发任务: {response['message']}")
except Exception as e:
print(f"SmartStrm 触发任务:出错 {e}")
print(f"SmartStrm 触发任务:出错 {str(e)}")

View File

@ -1,6 +1,6 @@
# !/usr/bin/env python3
# -*- coding: utf-8 -*-
# Modify: 2024-11-13
# Modify: 2025-09-05
# Repo: https://github.com/Cp0204/quark_auto_save
# ConfigFile: quark_config.json
"""
@ -96,20 +96,26 @@ class Config:
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "").split(",")
plugins_available = {}
task_plugins_config = {}
# 获取所有模块
py_ext = [".py", ".pyd"] if sys.platform == "win32" else [".py", ".so"]
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")
try:
with open(priority_path, encoding="utf-8") as f:
priority_modules = json.load(f)
if priority_modules:
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]
except (FileNotFoundError, json.JSONDecodeError):
priority_modules = []
if priority_modules:
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]
# 加载模块
for module_name in all_modules:
if f"-{module_name}" in PLUGIN_FLAGS:
continue
@ -128,7 +134,6 @@ class Config:
task_plugins_config[module_name] = plugin.default_task_config
except (ImportError, AttributeError) as e:
print(f"载入模块 {module_name} 失败: {e}")
print()
return plugins_available, plugins_config, task_plugins_config
def breaking_change_update(config_data):
@ -339,7 +344,7 @@ class MagicRename:
if match := re.search(r"\{I+\}", filename):
magic_i = match.group()
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:
if re.match(pattern, filename):
return filename
@ -497,7 +502,9 @@ class Quark:
).json()
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 = []
page = 1
while True:
@ -515,6 +522,8 @@ class Quark:
"_fetch_share": _fetch_share,
"_fetch_total": "1",
"_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()
if response["code"] != 0:
@ -564,6 +573,8 @@ class Quark:
"_fetch_sub_dirs": "0",
"_sort": "file_type:asc,updated_at:desc",
"_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()
if response["code"] != 0:
@ -616,6 +627,8 @@ class Quark:
"__t": datetime.now().timestamp(),
}
response = self._send_request("GET", url, params=querystring).json()
if response["status"] != 200:
return response
if response["data"]["status"] == 2:
if retry_index > 0:
print()
@ -709,6 +722,7 @@ class Quark:
match_pwd = re.search(r"pwd=(\w+)", url)
passcode = match_pwd.group(1) if match_pwd else ""
# path: fid-name
# Legacy 20250905
paths = []
matches = re.findall(r"/(\w{32})-?([^/]+)?", url)
for match in matches:
@ -967,36 +981,46 @@ class Quark:
fid_list = [item["fid"] for item in need_save_list]
fid_token_list = [item["share_fid_token"] for item in need_save_list]
if fid_list:
save_file_return = self.save_file(
fid_list, fid_token_list, to_pdir_fid, pwd_id, stoken
)
err_msg = None
if save_file_return["code"] == 0:
task_id = save_file_return["data"]["task_id"]
query_task_return = self.query_task(task_id)
if query_task_return["code"] == 0:
# 建立目录树
for index, item in enumerate(need_save_list):
icon = self._get_file_icon(item)
tree.create_node(
f"{icon}{item['file_name_re']}",
item["fid"],
parent=pdir_fid,
data={
"file_name": item["file_name"],
"file_name_re": item["file_name_re"],
"fid": f"{query_task_return['data']['save_as']['save_as_top_fids'][index]}",
"path": f"{savepath}/{item['file_name_re']}",
"is_dir": item["dir"],
"obj_category": item.get("obj_category", ""),
},
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:
# 转存成功,查询转存结果
task_id = save_file_return["data"]["task_id"]
query_task_return = self.query_task(task_id)
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 = query_task_return["message"]
else:
err_msg = save_file_return["message"]
if err_msg:
add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n")
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):
icon = self._get_file_icon(item)
tree.create_node(
f"{icon}{item['file_name_re']}",
item["fid"],
parent=pdir_fid,
data={
"file_name": item["file_name"],
"file_name_re": item["file_name_re"],
"fid": f"{save_as_top_fids[index]}",
"path": f"{savepath}/{item['file_name_re']}",
"is_dir": item["dir"],
"obj_category": item.get("obj_category", ""),
},
)
return tree
def do_rename(self, tree, node_id=None):
@ -1091,6 +1115,7 @@ def do_save(account, tasklist=[]):
plugins, CONFIG_DATA["plugins"], task_plugins_config = Config.load_plugins(
CONFIG_DATA.get("plugins", {})
)
print()
print(f"转存账号: {account.nickname}")
# 获取全部保存目录fid
account.update_savepath_fid(tasklist)
@ -1158,6 +1183,13 @@ def do_save(account, tasklist=[]):
plugin.run(task, account=account, tree=is_new_tree) or task
)
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():

View File

@ -23,10 +23,10 @@
},
"$SHOW_MAGIC": {
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*",
"replace": "{II}.{TASKNAME}.{DATE}.第{E}期{PART}.{EXT}"
"replace": "{TASKNAME}.{SXX}E{II}.第{E}期{PART}.{EXT}"
},
"$TV_MAGIC": {
"pattern": "",
"pattern": ".*\\.(mp4|mkv|mov|m4v|avi|mpeg|ts)$",
"replace": "{TASKNAME}.{SXX}E{E}.{EXT}"
}
},