Compare commits

..

No commits in common. "main" and "v0.7.1" have entirely different histories.
main ... v0.7.1

16 changed files with 145 additions and 1164 deletions

View File

@ -1,12 +1,6 @@
# 使用官方 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
@ -14,12 +8,17 @@ 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

View File

@ -8,7 +8,7 @@
对于一些持续更新的资源,隔段时间去转存十分麻烦。 对于一些持续更新的资源,隔段时间去转存十分麻烦。
定期执行本脚本自动转存、文件名整理,配合 [SmartStrm](https://github.com/Cp0204/SmartStrm) / [OpenList](https://github.com/OpenListTeam/OpenList) , Emby 可达到自动追更的效果。🥳 定期执行本脚本自动转存、文件名整理,配合 Alist, rclone, 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] 插件模块化,允许自行开发和挂载[插件](./plugins) - [x] 媒体库模块化,用户可很方便地[开发自己的媒体库hook模块](./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 \
@ -107,13 +107,11 @@ 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` | 任务执行超时时间(秒),超时则任务结束 |
#### 一键更新 #### 一键更新
@ -130,17 +128,23 @@ 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/正则处理教程) 更多正则使用说明:[正则处理教程](https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程)
@ -163,40 +167,6 @@ 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块钱让我知道开源有价值。谢谢
@ -209,10 +179,4 @@ 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>

View File

@ -15,15 +15,12 @@ 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
@ -33,30 +30,10 @@ 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", "")
try: BUILD_TAG = os.environ.get("BUILD_TAG", "")
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:
@ -73,7 +50,6 @@ 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 = {}
@ -97,8 +73,6 @@ 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):
@ -259,19 +233,8 @@ 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()
net_data = config_data.get("source", {}).get("net", {}) try:
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")
@ -289,37 +252,18 @@ 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 search_results return jsonify(
return [] {"success": True, "source": "CloudSaver", "data": search_results}
)
def ps_search(): else:
if ps_data.get("server"): return jsonify({"success": True, "message": search.get("message")})
ps = PanSou(ps_data.get("server")) else:
return ps.search(query, deep == "1") base_url = base64.b64decode("aHR0cHM6Ly9zLjkxNzc4OC54eXo=").decode()
return [] url = f"{base_url}/task_suggestions?q={query}&d={deep}"
response = requests.get(url)
try: return jsonify(
search_results = [] {"success": True, "source": "网络公开", "data": response.json()}
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)}"})
@ -340,9 +284,7 @@ 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( share_detail = account.get_detail(pwd_id, stoken, pdir_fid, _fetch_share=1)
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(
@ -350,10 +292,7 @@ def get_share_detail():
) )
data = share_detail["data"] data = share_detail["data"]
data["paths"] = [ data["paths"] = 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
# 正则处理预览 # 正则处理预览
@ -376,9 +315,7 @@ def get_share_detail():
) )
for share_file in data["list"]: for share_file in data["list"]:
search_pattern = ( search_pattern = (
task["update_subdir"] task.get("update_subdir", "") if share_file["dir"] else pattern
if share_file["dir"] and task.get("update_subdir")
else pattern
) )
if re.search(search_pattern, share_file["file_name"]): if re.search(search_pattern, share_file["file_name"]):
# 文件名重命名,目录不重命名 # 文件名重命名,目录不重命名
@ -487,36 +424,7 @@ def add_task():
# 定时任务执行的函数 # 定时任务执行的函数
def run_python(args): def run_python(args):
logging.info(f">>> 定时运行任务") logging.info(f">>> 定时运行任务")
try: os.system(f"{PYTHON_PATH} {args}")
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 函数执行完成")
# 重新加载任务 # 重新加载任务
@ -532,10 +440,6 @@ 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()
@ -554,7 +458,7 @@ def reload_tasks():
def init(): def init():
global config_data, task_plugins_config_default global config_data, task_plugins_config_default
logging.info(">>> 初始化配置") logging.info(f">>> 初始化配置")
# 检查配置文件是否存在 # 检查配置文件是否存在
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)):
@ -592,8 +496,6 @@ 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,

View File

@ -1,6 +1,5 @@
import re import re
import requests import requests
from sdk.common import iso_to_cst
class CloudSaver: class CloudSaver:
@ -125,10 +124,6 @@ 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"))
@ -137,10 +132,9 @@ 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("channelId", ""), "channel": item.get("channel", ""),
"source": "CloudSaver" "channel_id": item.get("channelId", ""),
} }
) )
return clean_results return clean_results

View File

@ -1,16 +0,0 @@
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,97 +0,0 @@
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)

View File

@ -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,48 +196,3 @@ 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;
}
}

View File

@ -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.6 // @version 0.4
// @description 在夸克网盘分享页面添加推送到 QAS 的按钮 // @description 在夸克网盘分享页面添加推送到 QAS 的按钮
// @icon https://pan.quark.cn/favicon.ico // @icon https://pan.quark.cn/favicon.ico
// @author Cp0204 // @author Cp0204
@ -76,16 +76,16 @@
} }
} }
waitForElement('.pc-member-entrance', (PcMemberButton) => { waitForElement('.DetailLayout--client-download--FpyCkdW.ant-dropdown-trigger', (clientDownloadButton) => {
const qasSettingButton = document.createElement('div'); const qasSettingButton = document.createElement('div');
qasSettingButton.className = 'pc-member-entrance'; qasSettingButton.className = 'DetailLayout--client-download--FpyCkdW ant-dropdown-trigger';
qasSettingButton.innerHTML = 'QAS设置'; qasSettingButton.innerHTML = 'QAS设置';
qasSettingButton.addEventListener('click', () => { qasSettingButton.addEventListener('click', () => {
showQASSettingDialog(); showQASSettingDialog();
}); });
PcMemberButton.parentNode.insertBefore(qasSettingButton, PcMemberButton.nextSibling); clientDownloadButton.parentNode.insertBefore(qasSettingButton, clientDownloadButton.nextSibling);
}); });
} }
@ -112,8 +112,7 @@
// 获取数据函数 // 获取数据函数
function getData() { function getData() {
const currentUrl = window.location.href; const currentUrl = window.location.href;
const lastTitle = document.querySelector('.primary .bcrumb-filename:last-child')?.getAttribute('title') || null; taskname = currentUrl.lastIndexOf('-') > 0 ? decodeURIComponent(currentUrl.match(/.*\/[^-]+-(.+)$/)[1]).replace('*101', '-') : document.querySelector('.author-name').textContent;
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() : "";
@ -155,63 +154,6 @@
}, },
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) {
@ -234,34 +176,16 @@
} catch (e) { } catch (e) {
Swal.fire({ Swal.fire({
title: '解析响应失败', title: '解析响应失败',
html: `<small> text: `无法解析 JSON 响应: ${response.responseText}`,
响应状态: ${response.status}<br> icon: 'error'
响应内容: ${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: '无法连接到 QAS 服务器,请检查网络连接和服务器地址', text: error,
icon: 'error', icon: 'error'
confirmButtonText: '重新配置',
showCancelButton: true,
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
showQASSettingDialog();
}
}); });
} }
}); });

View File

@ -198,70 +198,28 @@
<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> 资源搜索</h2> <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>
</div> </div>
</div> </div>
<div class="form-group row mb-0" style="display:flex; align-items:center;"> <div class="form-group row">
<div data-toggle="collapse" data-target="#collapse_net" aria-expanded="true" aria-controls="collapse_net"> <label class="col-sm-2 col-form-label">服务器</label>
<div class="btn btn-block text-left"> <div class="col-sm-10">
<i class="bi bi-caret-right-fill"></i> 网络公开搜索 <input type="text" v-model="formData.source.cloudsaver.server" class="form-control" placeholder="资源搜索服务器地址,如 http://172.17.0.1:8008">
</div>
</div> </div>
</div> </div>
<div class="collapse show ml-3" id="collapse_net"> <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 d-flex align-items-center"> <input type="text" v-model="formData.source.cloudsaver.username" class="form-control" placeholder="用户名">
<input type="checkbox" class="form-check-input" v-model="formData.source.net.enable" placeholder="是否启用网络公开搜索,默认启用">
</div>
</div> </div>
</div> </div>
<div class="form-group row mb-0" style="display:flex; align-items:center;"> <div class="form-group row">
<div data-toggle="collapse" data-target="#collapse_cloudsaver" aria-expanded="true" aria-controls="collapse_cloudsaver"> <label class="col-sm-2 col-form-label">密码</label>
<div class="btn btn-block text-left"> <div class="col-sm-10">
<i class="bi bi-caret-right-fill"></i> CloudSaver <input type="password" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
<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="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 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>
@ -310,10 +268,9 @@
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<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" v-if="task.shareurl_ban" :title="task.shareurl_ban" disabled><i class="bi bi-exclamation-triangle-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" @click="runScriptNow(index)" title="运行此任务" v-else><i class="bi bi-play-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" @click="removeTask(index)" title="删除此任务"><i class="bi bi-trash3-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">
@ -324,15 +281,12 @@
<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 ? `以下资源来自网络搜索,请自行辨识,如有侵权请联系资源方` : "未搜索到资源" }}</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 ? `以下资源来自 ${smart_param.taskSuggestions.source} 搜索,请自行辨识,如有侵权请联系资源方` : "未搜索到资源" }}</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="深度搜索">
@ -353,7 +307,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.switchShare=false;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.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>
@ -378,9 +332,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.switchShare=false;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.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" @dblclick="inputRawMagicRegex(task)" title="双击可将魔法匹配释放为填入原始正则表达式"> <input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex">
<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">
@ -399,7 +353,7 @@
<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.switchShare=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.previewRegex=false;fileSelect.sortBy='updated_at';fileSelect.sortOrder='desc';showShareSelect(index)">选择</button>
</div> </div>
</div> </div>
</div> </div>
@ -447,10 +401,7 @@
</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>
@ -499,34 +450,6 @@
</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>
<!-- 正则处理表达式 --> <!-- 正则处理表达式 -->
@ -581,7 +504,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 class="cursor-pointer text-muted" @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td> <td v-if="!fileSelect.selectShare"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td>
</template> </template>
</tr> </tr>
</tbody> </tbody>
@ -596,19 +519,6 @@
</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">&times;</span>
</button>
</div>
</div>
</div>
</div> </div>
@ -627,21 +537,14 @@
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: "",
@ -671,14 +574,12 @@
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", sortBy: "updated_at",
sortOrder: "desc" sortOrder: "desc"
@ -775,16 +676,6 @@
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;
@ -817,10 +708,8 @@
.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 => {
@ -1041,7 +930,6 @@
} }
}, },
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;
@ -1071,9 +959,7 @@
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() {
@ -1083,7 +969,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]) {
this.showToast(`魔法名 [${newKey}] 已存在,请使用其他名称`, 'warning'); alert(`魔法名 [${newKey}] 已存在,请使用其他名称`);
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]);
@ -1103,7 +989,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 {
this.showToast('删除失败:' + response.data.message, 'error'); alert('删除失败:' + response.data.message);
} }
}).catch(error => { }).catch(error => {
console.error('Error /delete_file:', error); console.error('Error /delete_file:', error);
@ -1134,7 +1020,6 @@
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 = [];
@ -1182,23 +1067,6 @@
$('#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) {
@ -1240,9 +1108,9 @@
} 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.split('#')[0]}#/list/share/${dir.fid}` shareurl = `${shareurl}/${dir.fid}-${dir.name?.replace('-', '*101')}`
} else { } else {
shareurl = `${shareurl.split('#')[0]}#/list/share/${dir.fid}` shareurl = `${shareurl}#/list/share/${dir.fid}-${dir.name?.replace('-', '*101')}`
} }
return shareurl; return shareurl;
}, },
@ -1265,86 +1133,7 @@
if (valA > valB) return this.fileSelect.sortOrder === "asc" ? 1 : -1; if (valA > valB) return this.fileSelect.sortOrder === "asc" ? 1 : -1;
return 0; 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>

View File

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

View File

@ -164,9 +164,7 @@ class Alist_sync:
# 获取网盘已有文件 # 获取网盘已有文件
source_dir_list = self.get_path_list(self.source_path) 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 == "": if self.tv_mode == 0 or self.tv_mode == "":
self.tv_mode = False self.tv_mode = False
else: else:
@ -230,10 +228,6 @@ class Alist_sync:
.lower() .lower()
) )
for target_list in target_dir_list: for target_list in target_dir_list:
if source_list["is_dir"]:
# print(f"跳过目录同步")
skip = True
break
if self.tv_mode: if self.tv_mode:
target_list_filename = ( target_list_filename = (
target_list["name"] target_list["name"]

View File

@ -1,312 +0,0 @@
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}&timestamp={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 ""

Binary file not shown.

View File

@ -1,75 +0,0 @@
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)}")

View File

@ -1,6 +1,6 @@
# !/usr/bin/env python3 # !/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Modify: 2025-09-05 # Modify: 2024-11-13
# Repo: https://github.com/Cp0204/quark_auto_save # Repo: https://github.com/Cp0204/quark_auto_save
# ConfigFile: quark_config.json # ConfigFile: quark_config.json
""" """
@ -96,26 +96,20 @@ 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(ext, "") f.replace(".py", "") for f in os.listdir(plugins_dir) if f.endswith(".py")
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)
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): except (FileNotFoundError, json.JSONDecodeError):
priority_modules = [] 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: for module_name in all_modules:
if f"-{module_name}" in PLUGIN_FLAGS: if f"-{module_name}" in PLUGIN_FLAGS:
continue continue
@ -134,6 +128,7 @@ 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):
@ -200,9 +195,6 @@ class MagicRename:
"", "",
"", "",
"", "",
"",
"",
"",
] ]
def __init__(self, magic_regex={}, magic_variable={}): def __init__(self, magic_regex={}, magic_variable={}):
@ -267,14 +259,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:
name = name.replace(keyword, f"_{i:02d}_") # 替换为数字,方便排序 return 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 = [
# 强制加入`文件修改时间`字段供排序效果1无可排序字符时则按修改时间排序2和目录已有文件重名时始终在其后 # 强制加入`文件修改时间`字段供排序效果1无可排序字符时则按修改时间排序2和目录已有文件重名时始终在其后
f"{f['file_name_re']}_{f['updated_at']}" 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"]
] ]
@ -297,7 +289,7 @@ class MagicRename:
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_index.get( i = filename_index.get(
f"{file['file_name_re']}_{file['updated_at']}", 0 f"{file['file_name_re']}{file['updated_at']}", 0
) )
file["file_name_re"] = re.sub( file["file_name_re"] = re.sub(
match.group(), match.group(),
@ -307,11 +299,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()
@ -344,7 +336,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 = re.escape(filename).replace(re.escape(magic_i), pattern_i) pattern = filename.replace(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
@ -502,9 +494,7 @@ class Quark:
).json() ).json()
return response return response
def get_detail( def get_detail(self, pwd_id, stoken, pdir_fid, _fetch_share=0):
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:
@ -522,8 +512,6 @@ 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:
@ -573,8 +561,6 @@ 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:
@ -627,8 +613,6 @@ 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["status"] != 200:
return response
if response["data"]["status"] == 2: if response["data"]["status"] == 2:
if retry_index > 0: if retry_index > 0:
print() print()
@ -722,7 +706,6 @@ 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:
@ -892,9 +875,7 @@ class Quark:
# 添加符合的 # 添加符合的
for share_file in share_file_list: for share_file in share_file_list:
search_pattern = ( search_pattern = (
task["update_subdir"] task.get("update_subdir", "") if share_file["dir"] else pattern
if share_file["dir"] and task.get("update_subdir")
else pattern
) )
# 正则文件名匹配 # 正则文件名匹配
if re.search(search_pattern, share_file["file_name"]): if re.search(search_pattern, share_file["file_name"]):
@ -981,46 +962,36 @@ 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 = [] if save_file_return["code"] == 0:
while fid_list: task_id = save_file_return["data"]["task_id"]
# 分次转存100个/次因query_task返回save_as_top_fids最多100 query_task_return = self.query_task(task_id)
save_file_return = self.save_file( if query_task_return["code"] == 0:
fid_list[:100], fid_token_list[:100], to_pdir_fid, pwd_id, stoken # 建立目录树
) for index, item in enumerate(need_save_list):
fid_list = fid_list[100:] icon = self._get_file_icon(item)
fid_token_list = fid_token_list[100:] tree.create_node(
if save_file_return["code"] == 0: f"{icon}{item['file_name_re']}",
# 转存成功,查询转存结果 item["fid"],
task_id = save_file_return["data"]["task_id"] parent=pdir_fid,
query_task_return = self.query_task(task_id) data={
if query_task_return["code"] == 0: "file_name": item["file_name"],
save_as_top_fids.extend( "file_name_re": item["file_name_re"],
query_task_return["data"]["save_as"]["save_as_top_fids"] "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", ""),
},
) )
else:
err_msg = query_task_return["message"]
else: else:
err_msg = save_file_return["message"] err_msg = query_task_return["message"]
if err_msg: else:
add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n") err_msg = save_file_return["message"]
# 建立目录树 if err_msg:
if len(need_save_list) == len(save_as_top_fids): add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n")
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 return tree
def do_rename(self, tree, node_id=None): def do_rename(self, tree, node_id=None):
@ -1115,7 +1086,6 @@ 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)
@ -1183,13 +1153,6 @@ 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():

View File

@ -13,7 +13,7 @@
} }
}, },
"magic_regex": { "magic_regex": {
"$TV_REGEX": { "$TV": {
"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_MAGIC": { "$SHOW_PRO": {
"pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*", "pattern": "^(?!.*纯享)(?!.*加更)(?!.*抢先)(?!.*预告).*?第\\d+期.*",
"replace": "{TASKNAME}.{SXX}E{II}.第{E}期{PART}.{EXT}" "replace": "{II}.{TASKNAME}.{DATE}.第{E}期{PART}.{EXT}"
}, },
"$TV_MAGIC": { "$TV_PRO": {
"pattern": ".*\\.(mp4|mkv|mov|m4v|avi|mpeg|ts)$", "pattern": "",
"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_REGEX", "pattern": "$TV",
"replace": "", "replace": "",
"enddate": "2099-01-30", "enddate": "2099-01-30",
"update_subdir": "4k|1080p" "update_subdir": "4k|1080p"