重塑 WebUI,增加更多实用功能,引入数据库模块,支持记录和查看转存历史,优化并完善了部分功能

Merge pull request #16 from x1ao4/dev
This commit is contained in:
x1ao4 2025-05-18 00:01:04 +08:00 committed by GitHub
commit 418e87ded0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 6417 additions and 605 deletions

View File

@ -20,7 +20,8 @@ ARG BUILD_TAG=v${VERSION}
ENV BUILD_SHA=$BUILD_SHA ENV BUILD_SHA=$BUILD_SHA
ENV BUILD_TAG=$BUILD_TAG ENV BUILD_TAG=$BUILD_TAG
# 端口 # 端口配置 (可通过 -e PORT=xxxx 修改适用于桥接模式和host模式)
ENV PORT=5005
EXPOSE 5005 EXPOSE 5005
# 运行应用程序 # 运行应用程序

View File

@ -1,30 +1,32 @@
# 夸克自动转存 # 夸克自动转存
本项目是在 [Cp0204/quark-auto-save:0.5.3.1](https://github.com/Cp0204/quark-auto-save) 的基础上修改而来的(感谢 [Cp0204](https://github.com/Cp0204)),我增加了几个功能,新增功能的代码都是通过 AI 完成的,不保证功能的稳定性。主要的新增功能如下([详见](https://github.com/x1ao4/quark-auto-save-x/wiki) 本项目是在 [Cp0204/quark-auto-save:0.5.3.1](https://github.com/Cp0204/quark-auto-save) 的基础上修改而来的(感谢 [Cp0204](https://github.com/Cp0204)),我对整个 WebUI 进行了重塑,增加了更多实用功能,新增功能的代码都是通过 AI 完成的,不保证功能的稳定性。主要的新增功能如下([详见](https://github.com/x1ao4/quark-auto-save-x/wiki)
- **过滤项目**:通过在 `过滤规则` 里设置过滤词来过滤不需要转存的文件或文件夹。 - **过滤项目**:通过在 `过滤规则` 里设置过滤词来过滤不需要转存的文件或文件夹。
- **顺序命名**:通过使用包含 `{}` 的表达式(如 `乘风2025 - S06E{}`)自动切换为 `顺序命名` 模式,该模式将通过文件名与上传时间等信息对文件进行智能排序,然后按顺序对每个文件的 `{}` 赋予序号,实现顺序命名。 - **顺序命名**:通过使用包含 `{}` 的表达式(如 `乘风2025 - S06E{}`)自动切换为 `顺序命名` 模式,该模式将通过文件名与上传时间等信息对文件进行智能排序,然后按顺序对每个文件的 `{}` 赋予序号,实现顺序命名。
- **剧集命名**:通过使用包含 `[]` 的表达式(如 `黑镜 - S06E[]`)自动切换为 `剧集命名` 模式,该模式将从原始文件名中提取剧集编号,然后把提取的编号代入对应文件名的 `[]` 中,实现自动按剧集编号命名。 - **剧集命名**:通过使用包含 `[]` 的表达式(如 `黑镜 - S06E[]`)自动切换为 `剧集命名` 模式,该模式将从原始文件名中提取剧集编号,然后把提取的编号代入对应文件名的 `[]` 中,实现自动按剧集编号命名。
- **自动切换命名模式**:默认的命名模式依然为 `正则命名` 模式,现在会通过用户输入的 `匹配表达式` 自动实时判断和切换对应的模式。 - **自动切换命名模式**:默认的命名模式依然为 `正则命名` 模式,现在会通过用户输入的 `匹配表达式` 自动实时判断和切换对应的模式。
- **自定义集编号识别规则**:支持在系统配置页面编辑用于提取剧集编号的 `集编号识别规则` 表达式,扩展识别范围。 - **自定义集编号识别规则**:支持在系统配置页面编辑用于提取剧集编号的 `集编号识别规则` 表达式,扩展识别范围。
- **数据库**:引入 SQLite 数据库,记录和管理所有转存历史,便于查询和追踪。
- **转存记录**:支持通过 WebUI 的转存记录页面查看、查询历史转存记录的相关信息。
- **WebUI**:对整个 WebUI 进行了重塑增加了更多实用功能如文件选择和预览界面的排序功能、资源搜索的过滤功能、TMDB 和豆瓣搜索功能、页面视图切换功能、账号设置功能等等。
本项目修改后的版本为个人需求定制版,代码是通过 AI 完成的,未经过充分测试(能发现的 BUG 我基本解决了,但可能存在未发现的 BUG我会在后续的使用中继续完善功能。若你要使用本项目请知晓本人不是程序员我无法保证本项目的稳定性如果你在使用过程中发现了 BUG可以在 Issues 中提交,但不保证每个 BUG 都能被修复,请谨慎使用,风险自担。 本项目修改后的版本为个人需求定制版,目的是满足我自己的使用需求,某些(我不用的)功能可能会因为修改而出现 BUG不一定会被修复。若你要使用本项目,请知晓本人不是程序员,我无法保证本项目的稳定性,如果你在使用过程中发现了 BUG可以在 Issues 中提交,但不保证每个 BUG 都能被修复,请谨慎使用,风险自担。
夸克网盘签到、自动转存、命名整理、发推送提醒和刷新媒体库一条龙。 夸克网盘签到、自动转存、命名整理、发推送提醒和刷新媒体库一条龙。对于一些持续更新的资源,隔段时间去转存十分麻烦。定期执行本脚本自动转存、重命名整理,配合 Alist、rclone、Emby、Plex 可达到自动追更的效果。🥳
对于一些持续更新的资源,隔段时间去转存十分麻烦。
定期执行本脚本自动转存、文件名整理,配合 Alist、rclone、Emby 可达到自动追更的效果。🥳
注意!资源不会每时每刻更新,**严禁设定过高的定时运行频率!** 以免账号风控和给夸克服务器造成不必要的压力。 注意!资源不会每时每刻更新,**严禁设置过高的定时运行频率!**
以免账号风控或者给夸克服务器造成不必要的压力。
## 功能 ## 功能
- 部署方式 - 部署方式
- [x] 兼容青龙 - [x] 支持 Docker 部署WebUI 配置
- [x] 支持 Docker 独立部署WebUI 配置 - [x] 可能兼容青龙
- 分享链接 - 分享链接
- [x] 支持分享链接的子目录 - [x] 支持分享链接的子目录
- [x] 记录失效分享并跳过任务 - [x] 记录失效分享并跳过任务
- [x] 支持需提取码的分享链接 <sup>[?](https://github.com/x1ao4/quark-auto-save-x/wiki/使用技巧集锦#支持需提取码的分享链接)</sup> - [x] 支持需提取码的分享链接 <sup>[?](https://github.com/x1ao4/quark-auto-save-x/wiki/使用技巧集锦#支持需提取码的分享链接)</sup>
- [x] 智能搜索资源并自动填充 <sup>[?](https://github.com/x1ao4/quark-auto-save-x/wiki/CloudSaver搜索源)</sup> - [x] 智能搜索资源并自动填充**支持自动过滤失效链接** <sup>[?](https://github.com/x1ao4/quark-auto-save-x/wiki/CloudSaver搜索源)</sup>
- 文件管理 - 文件管理
- [x] 目标目录不存在时自动新建 - [x] 目标目录不存在时自动新建
@ -32,15 +34,17 @@
- [x] **过滤不需要转存的文件或文件夹** - [x] **过滤不需要转存的文件或文件夹**
- [x] 转存后文件名整理(正则命名、**顺序命名**、**剧集命名** - [x] 转存后文件名整理(正则命名、**顺序命名**、**剧集命名**
- [x] 可选忽略文件后缀 - [x] 可选忽略文件后缀
- [x] **数据库记录所有转存历史**
- 任务管理 - 任务管理
- [x] 支持多组任务 - [x] 支持多组任务
- [x] 任务结束期限,期限后不执行此任务 - [x] 任务结束期限,期限后不执行此任务
- [x] 可单独指定子任务星期几执行 - [x] 可单独指定子任务星期几执行
- [x] **支持通过任务名称跳转 TMDB、豆瓣相关搜索页面**
- 媒体库整合 - 媒体库整合
- [x] 根据任务名搜索 Emby 媒体库 - [x] 根据任务名搜索 Emby 媒体库
- [x] 追更或整理后自动刷新 Emby 媒体库 - [x] 追更或整理后自动刷新 Emby、Plex 媒体库
- [x] 媒体库模块化,用户可很方便地[开发自己的媒体库hook模块](./plugins) - [x] 媒体库模块化,用户可很方便地[开发自己的媒体库hook模块](./plugins)
- 其它 - 其它
@ -51,21 +55,20 @@
## 部署 ## 部署
### Docker 部署 ### Docker 部署
Docker 部署提供 WebUI 管理配置,图形化配置已能满足绝大多数需求。部署命令: Docker Run
```shell ```shell
docker run -d \ docker run -d \
--name quark-auto-save-x \ --name quark-auto-save-x \
-p 5005:5005 \ -p 5005:5005 \
-e WEBUI_USERNAME=自定义用户名 \
-e WEBUI_PASSWORD=自定义密码 \
-v /自定义配置文件的存储目录/quark-auto-save-x/config:/app/config \ -v /自定义配置文件的存储目录/quark-auto-save-x/config:/app/config \
-v /自定义生成文件的存储目录:/media \ # 可选,插件 alist_strm_gen 生成 strm 使用 -v /自定义生成文件的存储目录:/media \ # 可选,插件 alist_strm_gen 生成 strm 使用
-v /自定义下载文件的存储目录:/downloads \ # 可选,插件 aria2 下载文件使用
--restart unless-stopped \ --restart unless-stopped \
x1ao4/quark-auto-save-x:latest x1ao4/quark-auto-save-x:latest
``` ```
docker-compose.yml Docker Compose
```yaml ```yaml
version: "3.3" version: "3.3"
@ -75,23 +78,24 @@ services:
container_name: quark-auto-save-x container_name: quark-auto-save-x
ports: ports:
- 5005:5005 - 5005:5005
environment:
WEBUI_USERNAME: 自定义用户名
WEBUI_PASSWORD: 自定义密码
volumes: volumes:
- /自定义配置文件的存储目录/quark-auto-save-x/config:/app/config - /自定义配置文件的存储目录/quark-auto-save-x/config:/app/config
- /自定义生成文件的存储目录:/media # 可选,插件 alist_strm_gen 生成 strm 使用 - /自定义生成文件的存储目录:/media # 可选,插件 alist_strm_gen 生成 strm 使用
- /自定义下载文件的存储目录:/downloads # 可选,插件 aria2 下载文件使用
restart: unless-stopped restart: unless-stopped
networks: {} networks: {}
``` ```
管理地址http://yourhost:5005 默认管理地址http://yourhost:5005
默认管理用户名/密码admin
| 环境变量 | 说明 | | 环境变量 | 说明 |
| ---------------- | -------- | | ---------------- | -------- |
| `WEBUI_USERNAME` | 用户名 | | `WEBUI_USERNAME` | 用户名 |
| `WEBUI_PASSWORD` | 密码 | | `WEBUI_PASSWORD` | 密码 |
| `PLUGIN_FLAGS` | 插件标志,如使用 `-emby,-aria2` 来禁用某些插件 | | `PLUGIN_FLAGS` | 插件标志,如使用 `-emby,-aria2` 来禁用某些插件 |
| `PORT` | 端口Host 模式可使用此变量更换端口 |
### 青龙部署 ### 青龙部署
程序也支持以青龙定时任务的方式运行,但该方式无法使用 WebUI 管理任务,需手动修改配置文件。 程序也支持以青龙定时任务的方式运行,但该方式无法使用 WebUI 管理任务,需手动修改配置文件。
@ -118,7 +122,7 @@ networks: {}
### 刷新媒体库 ### 刷新媒体库
在有新转存时,可触发完成相应功能,如自动刷新媒体库、生成 .strm 文件等。配置指南:[插件配置](https://github.com/x1ao4/quark-auto-save-x/wiki/插件配置) 在有新转存时,可触发完成相应功能,如自动刷新媒体库、生成 .strm 文件等。配置指南:[插件配置](https://github.com/x1ao4/quark-auto-save-x/wiki/插件配置)
媒体库模块以插件的方式的集成,如果你有兴趣请参考[插件开发指南](https://github.com/Cp0204/quark-auto-save/tree/main/plugins)。 媒体库模块以插件的方式的集成,如果你有兴趣请参考[插件开发指南](https://github.com/x1ao4/quark-auto-save-x/tree/x/plugins)。
### 更多使用技巧 ### 更多使用技巧
请参考 Wiki [使用技巧集锦](https://github.com/x1ao4/quark-auto-save-x/wiki/使用技巧集锦) 请参考 Wiki [使用技巧集锦](https://github.com/x1ao4/quark-auto-save-x/wiki/使用技巧集锦)

View File

@ -16,7 +16,7 @@ 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 sdk.cloudsaver import CloudSaver from sdk.cloudsaver import CloudSaver
from datetime import timedelta from datetime import timedelta, datetime
import subprocess import subprocess
import requests import requests
import hashlib import hashlib
@ -29,12 +29,24 @@ import re
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, parent_dir) sys.path.insert(0, parent_dir)
from quark_auto_save import Quark from quark_auto_save import Quark
from quark_auto_save import Config from quark_auto_save import Config, format_bytes
# 添加导入全局extract_episode_number和sort_file_by_name函数 # 添加导入全局extract_episode_number和sort_file_by_name函数
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from quark_auto_save import extract_episode_number, sort_file_by_name from quark_auto_save import extract_episode_number, sort_file_by_name
# 导入数据库模块
try:
from app.sdk.db import RecordDB
except ImportError:
# 如果没有数据库模块,定义一个空类
class RecordDB:
def __init__(self, *args, **kwargs):
pass
def get_records(self, *args, **kwargs):
return {"records": [], "pagination": {"total_records": 0, "total_pages": 0, "current_page": 1, "page_size": 20}}
def get_app_ver(): def get_app_ver():
BUILD_SHA = os.environ.get("BUILD_SHA", "") BUILD_SHA = os.environ.get("BUILD_SHA", "")
@ -53,6 +65,8 @@ SCRIPT_PATH = os.environ.get("SCRIPT_PATH", "./quark_auto_save.py")
CONFIG_PATH = os.environ.get("CONFIG_PATH", "./config/quark_config.json") CONFIG_PATH = os.environ.get("CONFIG_PATH", "./config/quark_config.json")
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "") PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true" DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
# 从环境变量获取端口默认为5005
PORT = int(os.environ.get("PORT", "5005"))
config_data = {} config_data = {}
task_plugins_config_default = {} task_plugins_config_default = {}
@ -114,17 +128,24 @@ def login():
if request.method == "POST": if request.method == "POST":
username = config_data["webui"]["username"] username = config_data["webui"]["username"]
password = config_data["webui"]["password"] password = config_data["webui"]["password"]
input_username = request.form.get("username")
input_password = request.form.get("password")
# 验证用户名和密码 # 验证用户名和密码
if (username == request.form.get("username")) and ( if not input_username or not input_password:
password == request.form.get("password") logging.info(">>> 登录失败:用户名或密码为空")
): return render_template("login.html", message="用户名和密码不能为空")
elif username != input_username:
logging.info(f">>> 登录失败:用户名错误 {input_username}")
return render_template("login.html", message="用户名或密码错误")
elif password != input_password:
logging.info(f">>> 用户 {input_username} 登录失败:密码错误")
return render_template("login.html", message="用户名或密码错误")
else:
logging.info(f">>> 用户 {username} 登录成功") logging.info(f">>> 用户 {username} 登录成功")
session.permanent = True session.permanent = True
session["token"] = get_login_token() session["token"] = get_login_token()
return redirect(url_for("index")) return redirect(url_for("index"))
else:
logging.info(f">>> 用户 {username} 登录失败")
return render_template("login.html", message="登录失败")
if is_login(): if is_login():
return redirect(url_for("index")) return redirect(url_for("index"))
@ -154,7 +175,11 @@ def get_data():
if not is_login(): if not is_login():
return jsonify({"success": False, "message": "未登录"}) return jsonify({"success": False, "message": "未登录"})
data = Config.read_json(CONFIG_PATH) data = Config.read_json(CONFIG_PATH)
del data["webui"] # 发送webui信息但不发送密码原文
data["webui"] = {
"username": config_data["webui"]["username"],
"password": config_data["webui"]["password"]
}
data["api_token"] = get_login_token() data["api_token"] = get_login_token()
data["task_plugins_config_default"] = task_plugins_config_default data["task_plugins_config_default"] = task_plugins_config_default
return jsonify({"success": True, "data": data}) return jsonify({"success": True, "data": data})
@ -169,8 +194,15 @@ def update():
dont_save_keys = ["task_plugins_config_default", "api_token"] dont_save_keys = ["task_plugins_config_default", "api_token"]
for key, value in request.json.items(): for key, value in request.json.items():
if key not in dont_save_keys: if key not in dont_save_keys:
config_data.update({key: value}) if key == "webui":
# 更新webui凭据
config_data["webui"]["username"] = value.get("username", config_data["webui"]["username"])
config_data["webui"]["password"] = value.get("password", config_data["webui"]["password"])
else:
config_data.update({key: value})
Config.write_json(CONFIG_PATH, config_data) Config.write_json(CONFIG_PATH, config_data)
# 更新session token确保当前会话在用户名密码更改后仍然有效
session["token"] = get_login_token()
# 重新加载任务 # 重新加载任务
if reload_tasks(): if reload_tasks():
logging.info(f">>> 配置更新成功") logging.info(f">>> 配置更新成功")
@ -197,6 +229,9 @@ def run_script_now():
process_env["PYTHONIOENCODING"] = "utf-8" process_env["PYTHONIOENCODING"] = "utf-8"
if tasklist: if tasklist:
process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False) process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False)
# 添加原始任务索引的环境变量
if len(tasklist) == 1 and 'original_index' in request.json:
process_env["ORIGINAL_TASK_INDEX"] = str(request.json['original_index'])
process = subprocess.Popen( process = subprocess.Popen(
command, command,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@ -294,15 +329,23 @@ def is_date_format(number_str):
return False return False
@app.route("/get_share_detail", methods=["POST"]) # 获取分享详情接口
@app.route("/get_share_detail", methods=["GET", "POST"])
def get_share_detail(): def get_share_detail():
if not is_login(): if not is_login():
return jsonify({"success": False, "message": "未登录"}) return jsonify({"success": False, "message": "未登录"})
shareurl = request.json.get("shareurl", "")
stoken = request.json.get("stoken", "") # 支持GET和POST请求
if request.method == "GET":
shareurl = request.args.get("shareurl", "")
stoken = request.args.get("stoken", "")
else:
shareurl = request.json.get("shareurl", "")
stoken = request.json.get("stoken", "")
account = Quark("", 0) account = Quark("", 0)
# 设置account的必要属性 # 设置account的必要属性
account.episode_patterns = request.json.get("regex", {}).get("episode_patterns", []) account.episode_patterns = request.json.get("regex", {}).get("episode_patterns", []) if request.method == "POST" else []
pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl) pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl)
if not stoken: if not stoken:
@ -313,6 +356,10 @@ def get_share_detail():
share_detail["paths"] = paths share_detail["paths"] = paths
share_detail["stoken"] = stoken share_detail["stoken"] = stoken
# 如果是GET请求或者不需要预览正则直接返回分享详情
if request.method == "GET" or not request.json.get("regex"):
return jsonify({"success": True, "data": share_detail})
# 正则命名预览 # 正则命名预览
def preview_regex(share_detail): def preview_regex(share_detail):
regex = request.json.get("regex") regex = request.json.get("regex")
@ -408,7 +455,6 @@ def get_share_detail():
regex_pattern = re.escape(episode_pattern).replace('\\[\\]', '(\\d+)') regex_pattern = re.escape(episode_pattern).replace('\\[\\]', '(\\d+)')
else: else:
# 如果输入模式不包含[],则使用简单匹配模式,避免正则表达式错误 # 如果输入模式不包含[],则使用简单匹配模式,避免正则表达式错误
print(f"⚠️ 剧集命名模式中没有找到 [] 占位符,将使用简单匹配")
regex_pattern = "^" + re.escape(episode_pattern) + "(\\d+)$" regex_pattern = "^" + re.escape(episode_pattern) + "(\\d+)$"
# 实现高级排序算法 # 实现高级排序算法
@ -643,7 +689,7 @@ def init():
"username": os.environ.get("WEBUI_USERNAME") "username": os.environ.get("WEBUI_USERNAME")
or config_data.get("webui", {}).get("username", "admin"), or config_data.get("webui", {}).get("username", "admin"),
"password": os.environ.get("WEBUI_PASSWORD") "password": os.environ.get("WEBUI_PASSWORD")
or config_data.get("webui", {}).get("password", "admin123"), or config_data.get("webui", {}).get("password", "admin"),
} }
# 默认定时规则 # 默认定时规则
@ -659,7 +705,137 @@ def init():
Config.write_json(CONFIG_PATH, config_data) Config.write_json(CONFIG_PATH, config_data)
# 获取历史转存记录
@app.route("/history_records")
def get_history_records():
if not is_login():
return jsonify({"success": False, "message": "未登录"})
# 获取请求参数
page = int(request.args.get("page", 1))
page_size = int(request.args.get("page_size", 20))
sort_by = request.args.get("sort_by", "transfer_time")
order = request.args.get("order", "desc")
# 获取筛选参数
task_name_filter = request.args.get("task_name", "")
keyword_filter = request.args.get("keyword", "")
# 是否只请求所有任务名称
get_all_task_names = request.args.get("get_all_task_names", "").lower() in ["true", "1", "yes"]
# 初始化数据库
db = RecordDB()
# 如果请求所有任务名称,单独查询并返回
if get_all_task_names:
cursor = db.conn.cursor()
cursor.execute("SELECT DISTINCT task_name FROM transfer_records ORDER BY task_name")
all_task_names = [row[0] for row in cursor.fetchall()]
# 如果同时请求分页数据,继续常规查询
if page > 0 and page_size > 0:
result = db.get_records(
page=page,
page_size=page_size,
sort_by=sort_by,
order=order,
task_name_filter=task_name_filter,
keyword_filter=keyword_filter
)
# 添加所有任务名称到结果中
result["all_task_names"] = all_task_names
# 处理记录格式化
format_records(result["records"])
return jsonify({"success": True, "data": result})
else:
# 只返回任务名称
return jsonify({"success": True, "data": {"all_task_names": all_task_names}})
# 常规查询
result = db.get_records(
page=page,
page_size=page_size,
sort_by=sort_by,
order=order,
task_name_filter=task_name_filter,
keyword_filter=keyword_filter
)
# 处理记录格式化
format_records(result["records"])
return jsonify({"success": True, "data": result})
# 辅助函数:格式化记录
def format_records(records):
for record in records:
# 格式化时间戳为可读形式
if "transfer_time" in record:
try:
# 确保时间戳在合理范围内
timestamp = int(record["transfer_time"])
if timestamp > 9999999999: # 检测是否为毫秒级时间戳13位
timestamp = timestamp / 1000 # 转换为秒级时间戳
if 0 < timestamp < 4102444800: # 从1970年到2100年的合理时间戳范围
record["transfer_time_readable"] = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
else:
record["transfer_time_readable"] = "无效日期"
except (ValueError, TypeError, OverflowError):
record["transfer_time_readable"] = "无效日期"
if "modify_date" in record:
try:
# 确保时间戳在合理范围内
timestamp = int(record["modify_date"])
if timestamp > 9999999999: # 检测是否为毫秒级时间戳13位
timestamp = timestamp / 1000 # 转换为秒级时间戳
if 0 < timestamp < 4102444800: # 从1970年到2100年的合理时间戳范围
record["modify_date_readable"] = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
else:
record["modify_date_readable"] = "无效日期"
except (ValueError, TypeError, OverflowError):
record["modify_date_readable"] = "无效日期"
# 格式化文件大小
if "file_size" in record:
try:
record["file_size_readable"] = format_bytes(int(record["file_size"]))
except (ValueError, TypeError):
record["file_size_readable"] = "未知大小"
@app.route("/get_user_info")
def get_user_info():
if not is_login():
return jsonify({"success": False, "message": "未登录"})
user_info_list = []
for idx, cookie in enumerate(config_data["cookie"]):
account = Quark(cookie, idx)
account_info = account.init()
if account_info:
user_info_list.append({
"index": idx,
"nickname": account_info["nickname"],
"is_active": account.is_active
})
else:
user_info_list.append({
"index": idx,
"nickname": "",
"is_active": False
})
return jsonify({"success": True, "data": user_info_list})
if __name__ == "__main__": if __name__ == "__main__":
init() init()
reload_tasks() reload_tasks()
app.run(debug=DEBUG, host="0.0.0.0", port=5005) app.run(debug=DEBUG, host="0.0.0.0", port=PORT)

195
app/sdk/db.py Normal file
View File

@ -0,0 +1,195 @@
import os
import json
import sqlite3
import time
from datetime import datetime
class RecordDB:
def __init__(self, db_path="config/data.db"):
self.db_path = db_path
self.conn = None
self.init_db()
def init_db(self):
# 确保目录存在
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
# 创建数据库连接
self.conn = sqlite3.connect(self.db_path)
cursor = self.conn.cursor()
# 创建表,如果不存在
cursor.execute('''
CREATE TABLE IF NOT EXISTS transfer_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
transfer_time INTEGER NOT NULL,
task_name TEXT NOT NULL,
original_name TEXT NOT NULL,
renamed_to TEXT NOT NULL,
file_size INTEGER NOT NULL,
duration TEXT,
resolution TEXT,
modify_date INTEGER NOT NULL,
file_id TEXT,
file_type TEXT
)
''')
self.conn.commit()
def close(self):
if self.conn:
self.conn.close()
def add_record(self, task_name, original_name, renamed_to, file_size, modify_date,
duration="", resolution="", file_id="", file_type=""):
"""添加一条转存记录"""
cursor = self.conn.cursor()
cursor.execute(
"INSERT INTO transfer_records (transfer_time, task_name, original_name, renamed_to, file_size, "
"duration, resolution, modify_date, file_id, file_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(int(time.time()), task_name, original_name, renamed_to, file_size,
duration, resolution, modify_date, file_id, file_type)
)
self.conn.commit()
return cursor.lastrowid
def update_renamed_to(self, file_id, original_name, renamed_to, task_name=""):
"""更新最近一条记录的renamed_to字段
Args:
file_id: 文件ID
original_name: 原文件名
renamed_to: 重命名后的文件名
task_name: 任务名称可选项如提供则作为附加筛选条件
Returns:
更新的记录数量
"""
cursor = self.conn.cursor()
# 构建查询条件
conditions = []
params = []
if file_id:
conditions.append("file_id = ?")
params.append(file_id)
if original_name:
conditions.append("original_name = ?")
params.append(original_name)
if task_name:
conditions.append("task_name = ?")
params.append(task_name)
# 如果没有提供有效的识别条件则返回0表示未更新任何记录
if not conditions:
return 0
# 构建WHERE子句
where_clause = " AND ".join(conditions)
# 查找最近添加的匹配记录
query = f"SELECT id FROM transfer_records WHERE {where_clause} ORDER BY transfer_time DESC LIMIT 1"
cursor.execute(query, params)
result = cursor.fetchone()
if result:
record_id = result[0]
# 更新记录
cursor.execute(
"UPDATE transfer_records SET renamed_to = ? WHERE id = ?",
(renamed_to, record_id)
)
self.conn.commit()
return cursor.rowcount
return 0
def get_records(self, page=1, page_size=20, sort_by="transfer_time", order="desc",
task_name_filter="", keyword_filter=""):
"""获取转存记录列表,支持分页、排序和筛选
Args:
page: 当前页码
page_size: 每页记录数
sort_by: 排序字段
order: 排序方向asc/desc
task_name_filter: 任务名称筛选条件精确匹配
keyword_filter: 关键字筛选条件模糊匹配任务名
"""
cursor = self.conn.cursor()
offset = (page - 1) * page_size
# 构建SQL查询
valid_columns = ["transfer_time", "task_name", "original_name", "renamed_to",
"file_size", "duration", "resolution", "modify_date"]
if sort_by not in valid_columns:
sort_by = "transfer_time"
order_direction = "DESC" if order.lower() == "desc" else "ASC"
# 构建筛选条件
where_clauses = []
params = []
if task_name_filter:
where_clauses.append("task_name = ?")
params.append(task_name_filter)
if keyword_filter:
where_clauses.append("task_name LIKE ?")
params.append(f"%{keyword_filter}%")
where_clause = " AND ".join(where_clauses)
where_sql = f"WHERE {where_clause}" if where_clause else ""
# 获取筛选后的总记录数
count_sql = f"SELECT COUNT(*) FROM transfer_records {where_sql}"
cursor.execute(count_sql, params)
total_records = cursor.fetchone()[0]
# 获取分页数据
query_sql = f"SELECT * FROM transfer_records {where_sql} ORDER BY {sort_by} {order_direction} LIMIT ? OFFSET ?"
cursor.execute(query_sql, params + [page_size, offset])
records = cursor.fetchall()
# 将结果转换为字典列表
columns = [col[0] for col in cursor.description]
result = []
for row in records:
record = dict(zip(columns, row))
result.append(record)
# 计算总页数
total_pages = (total_records + page_size - 1) // page_size if total_records > 0 else 1
return {
"records": result,
"pagination": {
"total_records": total_records,
"total_pages": total_pages,
"current_page": page,
"page_size": page_size
}
}
def get_record_by_id(self, record_id):
"""根据ID获取特定记录"""
cursor = self.conn.cursor()
cursor.execute("SELECT * FROM transfer_records WHERE id = ?", (record_id,))
record = cursor.fetchone()
if record:
columns = [col[0] for col in cursor.description]
return dict(zip(columns, record))
return None
def delete_record(self, record_id):
"""删除特定记录"""
cursor = self.conn.cursor()
cursor.execute("DELETE FROM transfer_records WHERE id = ?", (record_id,))
self.conn.commit()
return cursor.rowcount

21
app/static/Douban.svg Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 499.4 492.4" style="enable-background:new 0 0 499.4 492.4;" xml:space="preserve">
<g>
<path d="M25.7,9.2c14.8-1.1,29.7-0.4,44.6-0.5c133.3,0,266.7,0.1,400-0.1c5.5,0.1,11.8,0.2,16.5,3.5c5.1,5.8,2.8,14.5,3.7,21.5
c-0.4,6.9,1.3,15.6-4.2,21c-4.4,4.5-11.3,3.7-17,4c-135,0-270,0-405,0c-13.4-0.2-26.9,0.8-40.3-0.8c-4.6-0.8-7.7-4.9-8.3-9.4
c-0.9-10.2-1-20.5-0.1-30.7C16.3,12.7,20.7,9.2,25.7,9.2z"/>
<path d="M56.1,111.5c3.7-4.4,10.2-3.4,15.2-3.8c120,0,240,0,360.1,0c5.2,0.3,13-0.7,16,5c2.7,8.7,1.8,18,2.1,27
c-0.3,56.7,0.2,113.3-0.2,170c0.1,6.4-3.9,13.3-10.5,14.3c-12.5,1.3-25.2,0.6-37.7,0.6c-3.7,8.4-5.7,17.4-8.5,26.1
c-6.9,22.2-13.3,44.7-20.3,66.9c-2,6-3.2,12.2-4.3,18.4c35.5,1.7,71,0,106.5,0.8c5.7-0.4,13.4,1.4,15.1,7.8
c1.5,8.9,1.1,18.1,0.8,27.2c-0.6,5-3.9,10.8-9.3,11.5c-14.8,1.1-29.8,0.3-44.6,0.5c-138.3,0-276.7-0.1-415.1,0.1
c-5.1,0.2-10.4-3.9-10.9-9c-1.5-11.1-2.8-23,1.3-33.7c4-4.9,11.1-4.1,16.6-4.4c32.3-0.3,64.7,0.5,97-0.3
c-0.8-9.9-4.4-19.2-6.9-28.7c-5.2-19.1-12-37.9-15.1-57.6c18.7-0.5,37.4-0.7,56.1-0.3c4.2-0.5,9,0.8,11.4,4.5
c4,6.9,6.3,14.5,9.2,21.8c8.2,20.2,15.5,40.7,24.3,60.7c27-0.2,54.1,0.2,81.1,0c3.9,0.4,5.3-3.9,6.4-6.8
c7.9-23.4,16.9-46.5,24.9-69.9c3.8-11.7,9.3-22.9,11.8-35c-87-1.1-174.1,0.1-261.2-0.6c-4.2,0.4-8.6-1.3-10.9-4.9
c-4.1-6.9-2.6-15.3-3.2-22.9c0.1-56.7,0-113.4,0-170C53.7,121.7,53,115.8,56.1,111.5 M131.7,156.6c-2.5,17.6-1.1,35.4-1.4,53.1
c0.3,21.6-1.1,43.2,1.2,64.8c79.5,0.7,158.9-0.1,238.4,0.4c4.1-12.6,1.9-26.2,2.4-39.2c-0.4-24.9,0.8-49.8-0.5-74.6
c-0.4-4.4-6.3-4.7-9.5-4.4C285.5,156.6,208.6,156.9,131.7,156.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

8
app/static/TMDB.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 74 29.9" style="enable-background:new 0 0 74 29.9;" xml:space="preserve">
<title>Asset 4</title>
<path d="M15,27.1h44c6.7,0,12.2-5.4,12.2-12.2l0,0l0,0c0-6.7-5.4-12.2-12.1-12.2c0,0,0,0,0,0H15C8.3,2.8,2.9,8.2,2.9,15c0,0,0,0,0,0
l0,0C2.9,21.7,8.3,27.1,15,27.1L15,27.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 553 B

View File

@ -1,168 +0,0 @@
body {
font-size: 1rem;
padding-bottom: 110px;
}
@media (max-width: 768px) {
.container-fluid {
padding-right: 5px;
padding-left: 5px;
}
}
@media (min-width: 1360px) {
.container-fluid {
max-width: 1360px;
margin: 0 auto;
}
}
.bottom-buttons {
z-index: 99;
position: fixed;
left: 50%;
bottom: 20px;
background-color: transparent;
display: flex;
flex-direction: row;
transform: translateX(-50%);
}
.bottom-buttons button {
border-radius: 50%;
margin: 0 10px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border: none;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.title {
margin-top: 30px;
margin-bottom: 10px;
}
table.jsoneditor-tree>tbody>tr.jsoneditor-expandable:first-child {
display: none;
}
.modal-dialog {
max-width: 800px;
}
.modal-body {
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.task-suggestions {
width: 100%;
max-height: 250px;
overflow-y: auto;
transform: translate(0, -100%);
top: 0;
margin-top: -5px;
border: 1px solid #007bff;
z-index: 1021;
}
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
/* Behind the navbar */
padding: 54px 0 0;
/* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 54px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto;
/* Scrollable contents if viewport is shorter than content. */
}
@supports ((position: -webkit-sticky) or (position: sticky)) {
.sidebar-sticky {
position: -webkit-sticky;
position: sticky;
}
}
.sidebar .nav-link {
font-size: medium;
color: #333;
padding: 10px;
transition: background-color 0.3s ease;
/* 添加过渡效果 */
}
.sidebar .nav-link:hover {
background-color: #e0f0ff;
/* 改变背景颜色 */
}
.sidebar .nav-link i {
margin-right: 10px;
margin-left: 10px;
}
.sidebar .nav-link.active {
background-color: #007bff;
color: white !important;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
.navbar .navbar-toggler {
right: 1rem;
}
.navbar .form-control {
padding: .75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}
.cursor-pointer {
cursor: pointer
}

3720
app/static/css/main.css Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 103 KiB

File diff suppressed because it is too large Load Diff

View File

@ -4,80 +4,33 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title> <title>夸克自动转存 - 登录</title>
<link rel="stylesheet" href="./static/css/bootstrap.min.css"> <link rel="stylesheet" href="./static/css/bootstrap.min.css">
<link rel="stylesheet" href="./static/css/bootstrap-icons.min.css"> <link rel="stylesheet" href="./static/css/bootstrap-icons.min.css">
<style> <link rel="stylesheet" href="./static/css/main.css">
body {
background: linear-gradient(135deg, #c4d7ff 0%, #7996ff 100%);
min-height: 100vh;
display: flex;
align-items: center;
}
.login-card {
max-width: 400px;
width: 100%;
margin: 0 auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
border-radius: 10px;
overflow: hidden;
}
.login-header {
background: rgba(255, 255, 255, 0.9);
padding: 1rem 2rem;
text-align: center;
}
.login-body {
background: #fff;
padding: 2rem;
}
.btn {
border-radius: 20px;
padding: 10px 20px;
width: 100%;
}
</style>
</head> </head>
<body> <body class="login-page">
<div class="container"> <div class="login-card">
<div class="login-card"> <div class="login-header">
<div class="login-header"> <h1 class="login-title"><a href="https://github.com/x1ao4/quark-auto-save-x" target="_blank">夸克自动转存</a></h1>
<h1 class="mb-3">登录</h1> <p class="login-subtitle">欢迎回来,请登录您的账号</p>
<p class="text-muted">欢迎回来,请登录您的账户</p> </div>
<div class="login-body">
{% if message %}
<div class="alert" role="alert">
[[ message ]]
</div> </div>
<div class="login-body"> {% endif %}
{% if message %} <form action="/login" method="POST">
<div class="alert alert-danger text-center" role="alert"> <div class="form-group">
[[ message ]] <input type="text" class="form-control" id="username" name="username" placeholder="用户名" required>
</div> </div>
{% endif %} <div class="form-group">
<form action="/login" method="POST"> <input type="password" class="form-control" id="password" name="password" placeholder="密码" required>
<div class="form-group mb-3"> </div>
<label for="username" class="form-label">用户名</label> <button type="submit" class="btn">登录</button>
<div class="input-group"> </form>
<div class="input-group-prepend">
<span class="input-group-text"><i class="bi bi-person-fill"></i></span>
</div>
<input type="text" class="form-control" id="username" name="username" required>
</div>
</div>
<div class="form-group mb-4">
<label for="password" class="form-label">密码</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
</div>
<input type="password" class="form-control" id="password" name="password" required>
</div>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
</div>
</div> </div>
</div> </div>
</body> </body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

View File

@ -1,5 +1,22 @@
import os import os
import requests import requests
import sys
import re
import time
# 添加对全局排序函数的引用
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
from quark_auto_save import sort_file_by_name
except ImportError:
# 如果无法导入,提供一个简单的排序函数作为替代
def sort_file_by_name(file):
if isinstance(file, dict):
filename = file.get("file_name", "")
else:
filename = file
# 简单排序,主要通过文件名进行
return filename
class Aria2: class Aria2:
@ -7,7 +24,7 @@ class Aria2:
default_config = { default_config = {
"host_port": "172.17.0.1:6800", # Aria2 RPC地址 "host_port": "172.17.0.1:6800", # Aria2 RPC地址
"secret": "", # Aria2 RPC 密钥 "secret": "", # Aria2 RPC 密钥
"dir": "/Downloads", # 下载目录需要Aria2有权限访问 "dir": "/downloads", # 下载目录需要Aria2有权限访问
} }
default_task_config = { default_task_config = {
"auto_download": False, # 是否自动添加下载任务 "auto_download": False, # 是否自动添加下载任务
@ -35,36 +52,149 @@ class Aria2:
) )
if not task_config.get("auto_download"): if not task_config.get("auto_download"):
return return
if (tree := kwargs.get("tree")) and (account := kwargs.get("account")):
# 按文件路径排序添加下载任务 account = kwargs.get("account")
nodes = sorted( if not account:
tree.all_nodes_itr(), key=lambda node: node.data.get("path", "") return
)
file_fids = [] # 获取重命名日志,优先使用传入的参数
file_paths = [] rename_logs = kwargs.get("rename_logs", [])
for node in nodes:
if not node.data.get("is_dir", True): # 从重命名日志中提取文件信息
file_fids.append(node.data.get("fid")) renamed_files = {}
file_paths.append(node.data.get("path")) for log in rename_logs:
download_return, cookie = account.download(file_fids) if "重命名:" in log and "" in log:
file_urls = [item["download_url"] for item in download_return["data"]] # 精确匹配分割
for index, file_url in enumerate(file_urls): parts = log.split("重命名:", 1)[1].strip()
file_path = file_paths[index] if "" in parts:
print(f"📥 Aria2下载: {file_path}") old_name, new_name = parts.split("", 1)
local_path = f"{self.dir}{file_paths[index]}" # 排除失败信息
aria2_params = [ if " 失败," in new_name:
[file_url], new_name = new_name.split(" 失败,")[0]
{ # 清理空白
"header": [ old_name = old_name.strip()
f"Cookie: {cookie or account.cookie}", new_name = new_name.strip()
f"User-Agent: {account.USER_AGENT}", renamed_files[old_name] = new_name
],
"out": os.path.basename(local_path), # 获取文件树,确定本次需要下载的文件
"dir": os.path.dirname(local_path), current_files_to_download = set()
"pause": task_config.get("pause"), savepath = f"/{task['savepath']}".replace('//', '/')
},
] # 从文件树获取文件
if tree := kwargs.get("tree"):
file_nodes = [node for node in tree.all_nodes_itr() if not node.data.get("is_dir", True)]
for node in file_nodes:
if hasattr(node, 'tag'):
# 兼容新旧格式 - 更智能地提取文件名
tag_text = node.tag
# 处理所有可能的图标前缀
for icon in ["🎞️", "📁", "🖼️", "🎵", "📄", "📦", "📝", "💬"]:
if tag_text.startswith(icon):
# 移除图标并裁剪空格
tag_text = tag_text[len(icon):].strip()
break
filename = tag_text
current_files_to_download.add(filename)
# 如果从树中没有获取到文件,使用重命名后的文件
if not current_files_to_download and renamed_files:
current_files_to_download = set(renamed_files.values())
# 检查是否获取到了需要下载的文件
if not current_files_to_download:
print("📝 Aria2: 未找到需要下载的文件,跳过下载")
return
# 获取保存路径下所有文件
if savepath not in account.savepath_fid:
print(f"📝 Aria2: 保存路径 {savepath} 不存在")
return
dir_fid = account.savepath_fid[savepath]
dir_files = account.ls_dir(dir_fid)
# 筛选出当次转存的文件
file_fids = []
file_paths = []
for file in dir_files:
if file.get("dir", False):
continue # 跳过目录
file_name = file.get("file_name", "")
# 检查文件是否是当次转存的文件
is_current_file = False
# 直接匹配文件名或重命名后的文件
if file_name in current_files_to_download or file_name in renamed_files.values():
is_current_file = True
if is_current_file:
file_fids.append(file["fid"])
file_paths.append(f"{savepath}/{file_name}")
if not file_fids:
print("📝 Aria2: 未能匹配到需要下载的文件")
return
# print(f"📝 Aria2: 准备下载 {len(file_fids)} 个文件")
download_return, cookie = account.download(file_fids)
if not download_return.get("data"):
print("📝 Aria2: 获取下载链接失败")
return
# 准备要下载的文件信息
download_items = []
file_urls = [item["download_url"] for item in download_return["data"]]
for index, file_url in enumerate(file_urls):
file_path = file_paths[index]
local_path = f"{self.dir}{file_paths[index]}"
download_items.append({
"file_path": file_path,
"file_url": file_url,
"local_path": local_path,
"sort_key": sort_file_by_name(os.path.basename(file_path))
})
# 使用全局排序函数对文件进行排序
download_items.sort(key=lambda x: x["sort_key"])
# 按排序后的顺序下载文件
for item in download_items:
file_path = item["file_path"]
file_url = item["file_url"]
local_path = item["local_path"]
# 检查文件是否已存在
if os.path.exists(local_path):
# 如果文件已存在,跳过此文件
# print(f"📥 Aria2下载: {file_path} (已存在,跳过)")
continue
print(f"📥 Aria2下载: {file_path}")
# 确保目录存在
os.makedirs(os.path.dirname(local_path), exist_ok=True)
aria2_params = [
[file_url],
{
"header": [
f"Cookie: {cookie or account.cookie}",
f"User-Agent: {account.USER_AGENT}",
],
"out": os.path.basename(local_path),
"dir": os.path.dirname(local_path),
"pause": task_config.get("pause"),
},
]
try:
self.add_uri(aria2_params) self.add_uri(aria2_params)
except Exception as e:
print(f"📥 Aria2添加下载任务失败: {e}")
def _make_rpc_request(self, method, params=None): def _make_rpc_request(self, method, params=None):
"""发出 JSON-RPC 请求.""" """发出 JSON-RPC 请求."""

View File

@ -1,7 +1,5 @@
# !/usr/bin/env python3 # !/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Modify: 2024-11-13
# Repo: https://github.com/Cp0204/quark_auto_save
# ConfigFile: quark_config.json # ConfigFile: quark_config.json
""" """
new Env('夸克自动追更'); new Env('夸克自动追更');
@ -18,6 +16,26 @@ import importlib
import urllib.parse import urllib.parse
from datetime import datetime from datetime import datetime
# 添加数据库导入
try:
from app.sdk.db import RecordDB
except ImportError:
# 如果直接运行脚本,路径可能不同
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from app.sdk.db import RecordDB
except ImportError:
# 定义一个空的RecordDB类以防止导入失败
class RecordDB:
def __init__(self, *args, **kwargs):
self.enabled = False
def add_record(self, *args, **kwargs):
pass
def close(self):
pass
# 全局的文件排序函数 # 全局的文件排序函数
def sort_file_by_name(file): def sort_file_by_name(file):
""" """
@ -425,6 +443,29 @@ def add_notify(text):
return text return text
# 格式化文件显示,统一图标和文件名之间的空格
def format_file_display(prefix, icon, name):
"""
格式化文件/文件夹的显示确保图标和名称之间只有一个空格
Args:
prefix: 树形结构的前缀"├── "
icon: 文件/文件夹图标
name: 文件/文件夹名称
Returns:
格式化后的显示字符串
"""
# 去除图标和名称中可能存在的空格
clean_icon = icon.strip() if icon else ""
clean_name = name.strip() if name else ""
# 如果有图标,确保图标和名称之间只有一个空格
if clean_icon:
return f"{prefix}{clean_icon} {clean_name}"
else:
return f"{prefix}{clean_name}"
# 定义一个通用的文件类型图标选择函数 # 定义一个通用的文件类型图标选择函数
def get_file_icon(file_name, is_dir=False): def get_file_icon(file_name, is_dir=False):
"""根据文件扩展名返回对应的图标""" """根据文件扩展名返回对应的图标"""
@ -987,8 +1028,15 @@ class Quark:
<= datetime.strptime(item["enddate"], "%Y-%m-%d").date() <= datetime.strptime(item["enddate"], "%Y-%m-%d").date()
) )
] ]
# 去掉每个路径开头的斜杠,确保格式一致
dir_paths = [path.lstrip('/') for path in dir_paths]
if not dir_paths: if not dir_paths:
return False return False
# 重新添加斜杠前缀,确保格式一致
dir_paths = [f"/{path}" for path in dir_paths]
dir_paths_exist_arr = self.get_fids(dir_paths) dir_paths_exist_arr = self.get_fids(dir_paths)
dir_paths_exist = [item["file_path"] for item in dir_paths_exist_arr] dir_paths_exist = [item["file_path"] for item in dir_paths_exist_arr]
# 比较创建不存在的 # 比较创建不存在的
@ -1050,12 +1098,201 @@ class Quark:
except Exception as e: except Exception as e:
print(f"转存测试失败: {str(e)}") print(f"转存测试失败: {str(e)}")
def save_transfer_record(self, task, file_info, renamed_to=""):
"""保存转存记录到数据库
Args:
task: 任务信息
file_info: 文件信息
renamed_to: 重命名后的名称
"""
try:
# 初始化数据库
db = RecordDB()
# 提取文件信息
original_name = file_info.get("file_name", "")
file_size = file_info.get("size", 0)
# 处理修改日期
# 检查updated_at是否为未来日期
current_time = int(time.time())
modify_date = file_info.get("updated_at", current_time)
# 如果修改日期是毫秒级时间戳,转换为秒级
if isinstance(modify_date, int) and modify_date > 9999999999:
modify_date = int(modify_date / 1000)
# 确保修改日期是合理的值(不是未来日期)
if modify_date > current_time:
# 使用当前时间作为备用值
modify_date = current_time
file_id = file_info.get("fid", "")
file_type = os.path.splitext(original_name)[1].lower().lstrip(".") if original_name else ""
# 如果没有重命名信息,使用原始名称
if not renamed_to:
renamed_to = original_name
# 提取视频信息(时长和分辨率)
duration = ""
resolution = ""
# 对常见视频格式添加时长和分辨率信息
video_exts = ["mp4", "mkv", "avi", "mov", "wmv", "flv", "m4v", "webm"]
if file_type in video_exts:
# 在实际应用中,这里可以通过媒体处理库提取时长和分辨率
# 目前只是添加占位符,未来可以扩展功能
pass
# 添加记录到数据库
db.add_record(
task_name=task.get("taskname", ""),
original_name=original_name,
renamed_to=renamed_to,
file_size=file_size,
modify_date=modify_date,
duration=duration,
resolution=resolution,
file_id=file_id,
file_type=file_type
)
# 关闭数据库连接
db.close()
except Exception as e:
print(f"保存转存记录失败: {e}")
# 添加一个新的函数功能与save_transfer_record相同但名称更清晰表示其用途
def create_transfer_record(self, task, file_info, renamed_to=""):
"""创建新的转存记录
此函数与save_transfer_record功能完全相同但名称更明确地表达了其目的
- 用于在文件初次转存时创建记录
Args:
task: 任务信息
file_info: 文件信息
renamed_to: 重命名后的名称如果有
"""
self.save_transfer_record(task, file_info, renamed_to)
def update_transfer_record(self, task, file_info, renamed_to):
"""更新转存记录的重命名信息
Args:
task: 任务信息
file_info: 文件信息
renamed_to: 重命名后的名称
"""
try:
# 初始化数据库
db = RecordDB()
# 提取信息用于查找记录
original_name = file_info.get("file_name", "")
file_id = file_info.get("fid", "")
task_name = task.get("taskname", "")
# 更新记录
updated = db.update_renamed_to(
file_id=file_id,
original_name=original_name,
renamed_to=renamed_to,
task_name=task_name
)
# 关闭数据库连接
db.close()
return updated > 0
except Exception as e:
print(f"更新转存记录失败: {e}")
return False
# 添加一个专门从重命名日志更新记录的方法
def update_transfer_record_from_log(self, task, rename_log):
"""从重命名日志中提取信息并更新记录
Args:
task: 任务信息
rename_log: 重命名日志格式为 "重命名: 旧名 → 新名"
"""
try:
# 使用字符串分割方法提取文件名,更可靠地获取完整文件名
if "重命名:" not in rename_log or "" not in rename_log:
return False
# 先分割出"重命名:"后面的部分
parts = rename_log.split("重命名:", 1)[1].strip()
# 再按箭头分割
if "" not in parts:
return False
old_name, new_name = parts.split("", 1)
# 如果新名称包含"失败",则是失败的重命名,跳过
if "失败" in new_name:
return False
# 处理可能的截断标记,只保留实际文件名部分
# 注意:只有明确是失败消息才应该截断
if " 失败," in new_name:
new_name = new_name.split(" 失败,")[0]
# 去除首尾空格
old_name = old_name.strip()
new_name = new_name.strip()
# 确保提取到的是完整文件名
if not old_name or not new_name:
return False
# 初始化数据库
db = RecordDB()
# 使用原文件名和任务名查找记录
task_name = task.get("taskname", "")
# 更新记录
updated = db.update_renamed_to(
file_id="", # 不使用file_id查询因为在日志中无法获取
original_name=old_name,
renamed_to=new_name,
task_name=task_name
)
# 关闭数据库连接
db.close()
return updated > 0
except Exception as e:
print(f"根据日志更新转存记录失败: {e}")
return False
# 批量处理重命名日志
def process_rename_logs(self, task, rename_logs):
"""处理重命名日志列表,更新数据库记录
Args:
task: 任务信息
rename_logs: 重命名日志列表
"""
for log in rename_logs:
if "重命名:" in log and "" in log and "失败" not in log:
self.update_transfer_record_from_log(task, log)
def do_save_task(self, task): def do_save_task(self, task):
# 判断资源失效记录 # 判断资源失效记录
if task.get("shareurl_ban"): if task.get("shareurl_ban"):
print(f"分享资源已失效:{task['shareurl_ban']}") print(f"分享资源已失效:{task['shareurl_ban']}")
add_notify(f"❗《{task['taskname']}》分享资源已失效:{task['shareurl_ban']}\n") add_notify(f"❗《{task['taskname']}》分享资源已失效:{task['shareurl_ban']}\n")
return return
# 标准化保存路径,去掉可能存在的首位斜杠,然后重新添加
savepath = task["savepath"].lstrip('/')
task["savepath"] = savepath # 更新任务中的路径,确保后续处理一致
# 提取链接参数 # 提取链接参数
pwd_id, passcode, pdir_fid, paths = self.extract_url(task["shareurl"]) pwd_id, passcode, pdir_fid, paths = self.extract_url(task["shareurl"])
if not pwd_id: if not pwd_id:
@ -1080,13 +1317,13 @@ class Quark:
else: else:
savepath_fids = self.get_fids([savepath]) savepath_fids = self.get_fids([savepath])
if not savepath_fids: if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}") # print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath) mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0: if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"] self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}") # print(f"保存路径新建成功:{savepath}")
else: else:
print(f"保存路径新建失败:{mkdir_result['message']}") # print(f"保存路径新建失败:{mkdir_result['message']}")
return return
else: else:
# 路径已存在直接设置fid # 路径已存在直接设置fid
@ -1254,13 +1491,13 @@ class Quark:
else: else:
savepath_fids = self.get_fids([savepath]) savepath_fids = self.get_fids([savepath])
if not savepath_fids: if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}") # print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath) mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0: if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"] self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}") # print(f"保存路径新建成功:{savepath}")
else: else:
print(f"保存路径新建失败:{mkdir_result['message']}") # print(f"保存路径新建失败:{mkdir_result['message']}")
return return
else: else:
# 路径已存在直接设置fid # 路径已存在直接设置fid
@ -1479,7 +1716,7 @@ class Quark:
# 合并子目录树 # 合并子目录树
tree.create_node( tree.create_node(
"📁" + share_file["file_name"], f"📁{share_file['file_name']}",
share_file["fid"], share_file["fid"],
parent=pdir_fid, parent=pdir_fid,
data={ data={
@ -1668,7 +1905,7 @@ class Quark:
# 添加目录到树中但不添加到保存列表 # 添加目录到树中但不添加到保存列表
if not tree.contains(share_file["fid"]): if not tree.contains(share_file["fid"]):
tree.create_node( tree.create_node(
"📁" + share_file["file_name"], f"📁{share_file['file_name']}",
share_file["fid"], share_file["fid"],
parent=pdir_fid, parent=pdir_fid,
data={ data={
@ -1825,7 +2062,7 @@ class Quark:
# 检查节点是否已存在于树中,避免重复添加 # 检查节点是否已存在于树中,避免重复添加
if not tree.contains(share_file["fid"]): if not tree.contains(share_file["fid"]):
tree.create_node( tree.create_node(
"📁" + share_file["file_name"], f"📁{share_file['file_name']}",
share_file["fid"], share_file["fid"],
parent=pdir_fid, parent=pdir_fid,
data={ data={
@ -1948,7 +2185,7 @@ class Quark:
# 不再自动添加任务名称前缀,尊重用户选择 # 不再自动添加任务名称前缀,尊重用户选择
# 保存到树中 # 保存到树中
saved_files.append(f"{icon}{display_name}") saved_files.append(format_file_display("", icon, display_name))
# 检查节点是否已存在于树中,避免重复添加 # 检查节点是否已存在于树中,避免重复添加
if not tree.contains(item["fid"]): if not tree.contains(item["fid"]):
tree.create_node( tree.create_node(
@ -1962,6 +2199,14 @@ class Quark:
}, },
) )
# 保存转存记录到数据库
if not item["dir"]: # 只记录文件,不记录文件夹
self.create_transfer_record(
task=task,
file_info=item,
renamed_to=item.get("save_name", item["file_name"])
)
# 移除通知生成由do_save函数统一处理 # 移除通知生成由do_save函数统一处理
# 顺序命名模式和剧集命名模式都不在此处生成通知 # 顺序命名模式和剧集命名模式都不在此处生成通知
else: else:
@ -2108,6 +2353,14 @@ class Quark:
# 移除直接打印的部分由do_save负责打印 # 移除直接打印的部分由do_save负责打印
# print(rename_log) # print(rename_log)
is_rename_count += 1 is_rename_count += 1
# 更新重命名记录到数据库只更新renamed_to字段
# 不在这里直接调用update_transfer_record而是在do_save中统一处理
# self.update_transfer_record(
# task=task,
# file_info=dir_file,
# renamed_to=save_name
# )
else: else:
error_msg = rename_return.get("message", "未知错误") error_msg = rename_return.get("message", "未知错误")
rename_log = f"重命名: {dir_file['file_name']}{save_name} 失败,{error_msg}" rename_log = f"重命名: {dir_file['file_name']}{save_name} 失败,{error_msg}"
@ -2134,13 +2387,13 @@ class Quark:
# 路径已存在直接设置fid # 路径已存在直接设置fid
savepath_fids = self.get_fids([savepath]) savepath_fids = self.get_fids([savepath])
if not savepath_fids: if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}") # print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath) mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0: if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"] self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}") # print(f"保存路径新建成功:{savepath}")
else: else:
print(f"保存路径新建失败:{mkdir_result['message']}") # print(f"保存路径新建失败:{mkdir_result['message']}")
return False, [] return False, []
else: else:
self.savepath_fid[savepath] = savepath_fids[0]["fid"] self.savepath_fid[savepath] = savepath_fids[0]["fid"]
@ -2383,6 +2636,15 @@ class Quark:
# 进行重命名操作,确保文件按照预览名称保存 # 进行重命名操作,确保文件按照预览名称保存
time.sleep(1) # 等待文件保存完成 time.sleep(1) # 等待文件保存完成
# 保存转存记录到数据库
for saved_item in need_save_list:
if not saved_item.get("dir", False): # 只记录文件,不记录文件夹
self.create_transfer_record(
task=task,
file_info=saved_item,
renamed_to=saved_item.get("save_name", saved_item["file_name"])
)
# 刷新目录列表以获取新保存的文件 # 刷新目录列表以获取新保存的文件
fresh_dir_file_list = self.ls_dir(self.savepath_fid[savepath]) fresh_dir_file_list = self.ls_dir(self.savepath_fid[savepath])
@ -2479,6 +2741,13 @@ class Quark:
if df["fid"] == dir_file["fid"]: if df["fid"] == dir_file["fid"]:
df["file_name"] = target_name df["file_name"] = target_name
break break
# 不在这里直接调用update_transfer_record而是在do_save中统一处理
# self.update_transfer_record(
# task=task,
# file_info=dir_file,
# renamed_to=target_name
# )
else: else:
# 收集错误日志但不打印 # 收集错误日志但不打印
error_log = f"重命名: {dir_file['file_name']}{target_name} 失败,{rename_result['message']}" error_log = f"重命名: {dir_file['file_name']}{target_name} 失败,{rename_result['message']}"
@ -2504,7 +2773,7 @@ class Quark:
# 对本地已有文件进行重命名(即使没有分享链接或处理失败也执行) # 对本地已有文件进行重命名(即使没有分享链接或处理失败也执行)
is_rename_count = 0 is_rename_count = 0
renamed_files = [] renamed_files = {}
# 使用一个列表收集所有需要重命名的操作 # 使用一个列表收集所有需要重命名的操作
rename_operations = [] rename_operations = []
@ -2552,6 +2821,15 @@ class Quark:
if df["fid"] == dir_file["fid"]: if df["fid"] == dir_file["fid"]:
df["file_name"] = new_name df["file_name"] = new_name
break break
# 记录已重命名的文件
already_renamed_files.add(new_name)
# 不在这里直接调用update_transfer_record而是在do_save中统一处理
# self.update_transfer_record(
# task=task,
# file_info=dir_file,
# renamed_to=new_name
# )
else: else:
# 收集错误日志但不打印 # 收集错误日志但不打印
error_msg = rename_return.get("message", "未知错误") error_msg = rename_return.get("message", "未知错误")
@ -2617,13 +2895,13 @@ class Quark:
# 路径不存在创建或获取fid # 路径不存在创建或获取fid
savepath_fids = self.get_fids([savepath]) savepath_fids = self.get_fids([savepath])
if not savepath_fids: if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}") # print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath) mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0: if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"] self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}") # print(f"保存路径新建成功:{savepath}")
else: else:
print(f"保存路径新建失败:{mkdir_result['message']}") # print(f"保存路径新建失败:{mkdir_result['message']}")
return False, [] return False, []
else: else:
self.savepath_fid[savepath] = savepath_fids[0]["fid"] self.savepath_fid[savepath] = savepath_fids[0]["fid"]
@ -2807,8 +3085,17 @@ def do_save(account, tasklist=[]):
# 执行任务 # 执行任务
for index, task in enumerate(tasklist): for index, task in enumerate(tasklist):
# 检查环境变量获取真实的任务索引(用于显示)
if len(tasklist) == 1 and os.environ.get("ORIGINAL_TASK_INDEX"):
try:
display_index = int(os.environ.get("ORIGINAL_TASK_INDEX"))
except (ValueError, TypeError):
display_index = index + 1
else:
display_index = index + 1
print() print()
print(f"#{index+1}------------------") print(f"#{str(display_index).zfill(2)}------------------")
print(f"任务名称: {task['taskname']}") print(f"任务名称: {task['taskname']}")
print(f"分享链接: {task['shareurl']}") print(f"分享链接: {task['shareurl']}")
print(f"保存路径: {task['savepath']}") print(f"保存路径: {task['savepath']}")
@ -2945,10 +3232,10 @@ def do_save(account, tasklist=[]):
renamed_files = {} renamed_files = {}
for log in rename_logs: for log in rename_logs:
# 格式:重命名: 旧名 → 新名 # 格式:重命名: 旧名 → 新名
match = re.search(r'重命名: (.*?) → (.*?)($|\s|)', log) match = re.search(r'重命名: (.*?) → (.+?)($|\s||失败)', log)
if match: if match:
old_name = match.group(1) old_name = match.group(1).strip()
new_name = match.group(2) new_name = match.group(2).strip()
renamed_files[old_name] = new_name renamed_files[old_name] = new_name
# 获取文件列表,只添加重命名的文件 # 获取文件列表,只添加重命名的文件
@ -3016,10 +3303,10 @@ def do_save(account, tasklist=[]):
renamed_files = {} renamed_files = {}
for log in rename_logs: for log in rename_logs:
# 格式:重命名: 旧名 → 新名 # 格式:重命名: 旧名 → 新名
match = re.search(r'重命名: (.*?) → (.*?)($|\s|)', log) match = re.search(r'重命名: (.*?) → (.+?)($|\s||失败)', log)
if match: if match:
old_name = match.group(1) old_name = match.group(1).strip()
new_name = match.group(2) new_name = match.group(2).strip()
renamed_files[old_name] = new_name renamed_files[old_name] = new_name
# 只显示重命名的文件 # 只显示重命名的文件
@ -3033,7 +3320,7 @@ def do_save(account, tasklist=[]):
# 获取适当的图标 # 获取适当的图标
icon = get_file_icon(new_filename, is_dir=node.data.get("is_dir", False)) icon = get_file_icon(new_filename, is_dir=node.data.get("is_dir", False))
# 添加到显示列表 # 添加到显示列表
display_files.append((f"{icon}{new_filename}", node)) display_files.append((f"{icon} {new_filename}", node))
else: else:
# 如果没有重命名日志,使用原来的顺序命名逻辑 # 如果没有重命名日志,使用原来的顺序命名逻辑
if task.get("use_sequence_naming") and task.get("sequence_naming"): if task.get("use_sequence_naming") and task.get("sequence_naming"):
@ -3056,7 +3343,7 @@ def do_save(account, tasklist=[]):
# 获取适当的图标 # 获取适当的图标
icon = get_file_icon(orig_filename, is_dir=node.data.get("is_dir", False)) icon = get_file_icon(orig_filename, is_dir=node.data.get("is_dir", False))
# 添加到显示列表 # 添加到显示列表
display_files.append((f"{icon}{new_filename}", node)) display_files.append((f"{icon} {new_filename}", node))
# 按数字排序 # 按数字排序
display_files.sort(key=lambda x: int(os.path.splitext(x[0].lstrip("🎞️"))[0]) if os.path.splitext(x[0].lstrip("🎞️"))[0].isdigit() else float('inf')) display_files.sort(key=lambda x: int(os.path.splitext(x[0].lstrip("🎞️"))[0]) if os.path.splitext(x[0].lstrip("🎞️"))[0].isdigit() else float('inf'))
@ -3066,10 +3353,10 @@ def do_save(account, tasklist=[]):
renamed_files = {} renamed_files = {}
for log in rename_logs: for log in rename_logs:
# 格式:重命名: 旧名 → 新名 # 格式:重命名: 旧名 → 新名
match = re.search(r'重命名: (.*?) → (.*?)($|\s|)', log) match = re.search(r'重命名: (.*?) → (.+?)($|\s||失败)', log)
if match: if match:
old_name = match.group(1) old_name = match.group(1).strip()
new_name = match.group(2) new_name = match.group(2).strip()
renamed_files[old_name] = new_name renamed_files[old_name] = new_name
# 使用已知的剧集命名模式来生成新文件名 # 使用已知的剧集命名模式来生成新文件名
@ -3091,7 +3378,7 @@ def do_save(account, tasklist=[]):
# 获取适当的图标 # 获取适当的图标
icon = get_file_icon(new_filename, is_dir=node.data.get("is_dir", False)) icon = get_file_icon(new_filename, is_dir=node.data.get("is_dir", False))
# 添加到显示列表 # 添加到显示列表
display_files.append((f"{icon}{new_filename}", node)) display_files.append((f"{icon} {new_filename}", node))
# 如果没有找到任何文件要显示,使用原始文件名 # 如果没有找到任何文件要显示,使用原始文件名
if not display_files: if not display_files:
@ -3100,7 +3387,7 @@ def do_save(account, tasklist=[]):
orig_filename = node.tag.lstrip("🎞️") orig_filename = node.tag.lstrip("🎞️")
# 添加适当的图标 # 添加适当的图标
icon = get_file_icon(orig_filename, is_dir=node.data.get("is_dir", False)) icon = get_file_icon(orig_filename, is_dir=node.data.get("is_dir", False))
display_files.append((f"{icon}{orig_filename}", node)) display_files.append((f"{icon} {orig_filename}", node))
else: else:
# 其他模式:显示原始文件名 # 其他模式:显示原始文件名
display_files = [] display_files = []
@ -3224,7 +3511,7 @@ def do_save(account, tasklist=[]):
# 添加文件节点到结构中 # 添加文件节点到结构中
dir_structure[parent_id].append({ dir_structure[parent_id].append({
"id": node.identifier, "id": node.identifier,
"name": f"{icon}{orig_filename}", "name": f"{icon} {orig_filename}",
"is_dir": False "is_dir": False
}) })
@ -3513,7 +3800,7 @@ def do_save(account, tasklist=[]):
else: else:
# 添加基本通知 # 添加基本通知
add_notify(f"✅《{task['taskname']}》添加追更:") add_notify(f"✅《{task['taskname']}》添加追更:")
add_notify(f"/{task['savepath']}") add_notify(f"{re.sub(r'/{2,}', '/', f'/{task['savepath']}')}")
# 修正首次运行时对子目录的处理 - 只有在首次运行且有新增的子目录时才显示子目录内容 # 修正首次运行时对子目录的处理 - 只有在首次运行且有新增的子目录时才显示子目录内容
if has_update_in_root and has_update_in_subdir and is_first_run and len(new_added_dirs) == 0: if has_update_in_root and has_update_in_subdir and is_first_run and len(new_added_dirs) == 0:
@ -3649,7 +3936,7 @@ def do_save(account, tasklist=[]):
dir_name != save_path_basename): dir_name != save_path_basename):
dir_prefix = prefix + ("└── " if is_dir_last else "├── ") dir_prefix = prefix + ("└── " if is_dir_last else "├── ")
add_notify(f"{dir_prefix}📁{dir_name}") add_notify(format_file_display(dir_prefix, "📁", dir_name))
# 计算子项的前缀,保持树形结构清晰 # 计算子项的前缀,保持树形结构清晰
# 第一个缩进标记使用点号,后续使用空格 # 第一个缩进标记使用点号,后续使用空格
@ -3676,7 +3963,7 @@ def do_save(account, tasklist=[]):
file_prefix = prefix + ("└── " if is_file_last else "├── ") file_prefix = prefix + ("└── " if is_file_last else "├── ")
file_name = file_node.tag.lstrip("🎞️") file_name = file_node.tag.lstrip("🎞️")
icon = get_file_icon(file_name, is_dir=False) icon = get_file_icon(file_name, is_dir=False)
add_notify(f"{file_prefix}{icon}{file_name}") add_notify(format_file_display(file_prefix, icon, file_name))
# 构建并显示目录树 # 构建并显示目录树
if has_update_in_root or has_update_in_subdir: if has_update_in_root or has_update_in_subdir:
@ -3710,9 +3997,10 @@ def do_save(account, tasklist=[]):
# 再按箭头分割 # 再按箭头分割
if "" in parts: if "" in parts:
old_name, new_name = parts.split("", 1) old_name, new_name = parts.split("", 1)
# 如果新名称包含空格或其他分隔符,只取第一个换行符之前的内容 # 只处理失败信息,不截断正常文件名
if "\n" in new_name: if " 失败," in new_name:
new_name = new_name.split("\n")[0] new_name = new_name.split(" 失败,")[0]
# 去除首尾空格
old_name = old_name.strip() old_name = old_name.strip()
new_name = new_name.strip() new_name = new_name.strip()
renamed_files[old_name] = new_name renamed_files[old_name] = new_name
@ -3794,7 +4082,7 @@ def do_save(account, tasklist=[]):
# 添加成功通知 - 修复问题:确保在有文件时添加通知 # 添加成功通知 - 修复问题:确保在有文件时添加通知
if display_files: if display_files:
add_notify(f"✅《{task['taskname']}》添加追更:") add_notify(f"✅《{task['taskname']}》添加追更:")
add_notify(f"/{task['savepath']}") add_notify(f"{re.sub(r'/{2,}', '/', f'/{task['savepath']}')}")
# 创建episode_pattern函数用于排序 # 创建episode_pattern函数用于排序
@ -3834,7 +4122,7 @@ def do_save(account, tasklist=[]):
icon = get_file_icon(file_name, is_dir=False) icon = get_file_icon(file_name, is_dir=False)
else: else:
icon = get_file_icon(file_name, is_dir=file_info.get("dir", False)) icon = get_file_icon(file_name, is_dir=file_info.get("dir", False))
add_notify(f"{prefix}{icon}{file_name}") add_notify(format_file_display(prefix, icon, file_name))
# 确保只有在有文件时才添加空行 # 确保只有在有文件时才添加空行
if display_files: if display_files:
@ -3852,10 +4140,10 @@ def do_save(account, tasklist=[]):
renamed_files = {} renamed_files = {}
for log in rename_logs: for log in rename_logs:
# 格式:重命名: 旧名 → 新名 # 格式:重命名: 旧名 → 新名
match = re.search(r'重命名: (.*?) → (.*?)($|\s|)', log) match = re.search(r'重命名: (.*?) → (.+?)($|\s||失败)', log)
if match: if match:
old_name = match.group(1) old_name = match.group(1).strip()
new_name = match.group(2) new_name = match.group(2).strip()
renamed_files[old_name] = new_name renamed_files[old_name] = new_name
# 只显示重命名的文件 # 只显示重命名的文件
@ -3871,48 +4159,48 @@ def do_save(account, tasklist=[]):
# 添加成功通知 # 添加成功通知
add_notify(f"✅《{task['taskname']}》添加追更:") add_notify(f"✅《{task['taskname']}》添加追更:")
add_notify(f"/{task['savepath']}") add_notify(f"{re.sub(r'/{2,}', '/', f'/{task['savepath']}')}")
# 打印文件列表 # 打印文件列表
for idx, file_name in enumerate(display_files): for idx, file_name in enumerate(display_files):
prefix = "├── " if idx < len(display_files) - 1 else "└── " prefix = "├── " if idx < len(display_files) - 1 else "└── "
file_info = file_nodes[next((i for i, f in enumerate(file_nodes) if f["file_name"] == file_name), 0)] file_info = file_nodes[next((i for i, f in enumerate(file_nodes) if f["file_name"] == file_name), 0)]
icon = get_file_icon(file_name, is_dir=file_info.get("dir", False)) icon = get_file_icon(file_name, is_dir=file_info.get("dir", False))
add_notify(f"{prefix}{icon}{file_name}") add_notify(format_file_display(prefix, icon, file_name))
add_notify("") add_notify("")
# 打印重命名日志(文件树之后) # 打印重命名日志(文件树之后)
if rename_logs: if rename_logs:
# 处理重命名日志,更新数据库记录
account.process_rename_logs(task, rename_logs)
# 对剧集命名模式和其他模式统一处理重命名日志 # 对剧集命名模式和其他模式统一处理重命名日志
# 按剧集号/顺序号排序重命名日志 # 按sort_file_by_name函数的多级排序逻辑排序重命名日志
sorted_rename_logs = [] sorted_rename_logs = []
for log in rename_logs: for log in rename_logs:
# 提取新文件名(格式:重命名: 旧名 → 新名) # 提取新文件名(格式:重命名: 旧名 → 新名)
match = re.search(r'\s+(.+?)($|\s|)', log) # 使用更精确的字符串分割方法,确保能捕获完整的文件名
if match: if "重命名:" in log and "" in log:
new_name = match.group(1) parts = log.split("重命名:", 1)[1].strip()
# 尝试提取序号 if "" in parts:
# 先尝试从文件名中提取序号 _, new_name = parts.split("", 1)
seq_match = re.search(r'[SE](\d+)|(\d+)[.集期话]', new_name)
if seq_match:
# 提取序号SE格式或数字+集/期/话)
seq_num = int(seq_match.group(1) or seq_match.group(2))
sorted_rename_logs.append((seq_num, log))
else:
# 尝试直接从文件名开头提取数字
seq_match = re.match(r'(\d+)', new_name)
if seq_match:
seq_num = int(seq_match.group(1))
sorted_rename_logs.append((seq_num, log))
else:
# 未找到序号的日志放在最后
sorted_rename_logs.append((999, log))
else:
# 没找到箭头的日志
sorted_rename_logs.append((999, log))
# 按序号排序 # 只处理失败信息,不截断正常文件名
if " 失败," in new_name:
new_name = new_name.split(" 失败,")[0]
# 去除首尾空格
new_name = new_name.strip()
# 使用sort_file_by_name函数获取排序值
sort_tuple = sort_file_by_name(new_name)
sorted_rename_logs.append((sort_tuple, log))
else:
# 没找到箭头或格式不符的日志放在最后
sorted_rename_logs.append(((float('inf'), float('inf'), float('inf'), 0), log))
# 按sort_file_by_name返回的排序元组排序
sorted_rename_logs.sort(key=lambda x: x[0]) sorted_rename_logs.sort(key=lambda x: x[0])
# 打印排序后的日志 # 打印排序后的日志
@ -3949,7 +4237,7 @@ def do_save(account, tasklist=[]):
for plugin_name, plugin in plugins.items(): for plugin_name, plugin in plugins.items():
if plugin.is_active and (is_new_tree or is_rename): if plugin.is_active and (is_new_tree or is_rename):
task = ( task = (
plugin.run(task, account=account, tree=is_new_tree) or task plugin.run(task, account=account, tree=is_new_tree, rename_logs=rename_logs) or task
) )
elif is_new_tree is False: # 明确没有新文件 elif is_new_tree is False: # 明确没有新文件
print(f"任务完成: 没有新的文件需要转存") print(f"任务完成: 没有新的文件需要转存")

View File

@ -1,6 +1,6 @@
{ {
"cookie": [ "cookie": [
"Your pan.quark.cn Cookie1, Only this one will do save task." "打开 pan.quark.com 按 F12 抓取"
], ],
"push_config": { "push_config": {
"QUARK_SIGN_NOTIFY": true, "QUARK_SIGN_NOTIFY": true,