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

This commit is contained in:
x1ao4 2025-05-17 17:04:39 +08:00
parent 52545aa890
commit 250deb4b5f
12 changed files with 6389 additions and 584 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

@ -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>

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:
@ -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(
@ -1961,6 +2198,14 @@ class Quark:
"is_dir": item["dir"], "is_dir": item["dir"],
}, },
) )
# 保存转存记录到数据库
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函数统一处理
# 顺序命名模式和剧集命名模式都不在此处生成通知 # 顺序命名模式和剧集命名模式都不在此处生成通知
@ -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格式或数字+集/期/话) if " 失败," in new_name:
seq_num = int(seq_match.group(1) or seq_match.group(2)) new_name = new_name.split(" 失败,")[0]
sorted_rename_logs.append((seq_num, log))
else: # 去除首尾空格
# 尝试直接从文件名开头提取数字 new_name = new_name.strip()
seq_match = re.match(r'(\d+)', new_name)
if seq_match: # 使用sort_file_by_name函数获取排序值
seq_num = int(seq_match.group(1)) sort_tuple = sort_file_by_name(new_name)
sorted_rename_logs.append((seq_num, log)) sorted_rename_logs.append((sort_tuple, log))
else:
# 未找到序号的日志放在最后
sorted_rename_logs.append((999, log))
else: else:
# 没找到箭头的日志 # 没找到箭头或格式不符的日志放在最后
sorted_rename_logs.append((999, log)) 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"任务完成: 没有新的文件需要转存")