重塑 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_TAG=$BUILD_TAG
# 端口
# 端口配置 (可通过 -e PORT=xxxx 修改适用于桥接模式和host模式)
ENV PORT=5005
EXPOSE 5005
# 运行应用程序

View File

@ -16,7 +16,7 @@ from flask import (
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from sdk.cloudsaver import CloudSaver
from datetime import timedelta
from datetime import timedelta, datetime
import subprocess
import requests
import hashlib
@ -29,12 +29,24 @@ import re
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, parent_dir)
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函数
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
# 导入数据库模块
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():
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")
PLUGIN_FLAGS = os.environ.get("PLUGIN_FLAGS", "")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
# 从环境变量获取端口默认为5005
PORT = int(os.environ.get("PORT", "5005"))
config_data = {}
task_plugins_config_default = {}
@ -114,17 +128,24 @@ def login():
if request.method == "POST":
username = config_data["webui"]["username"]
password = config_data["webui"]["password"]
input_username = request.form.get("username")
input_password = request.form.get("password")
# 验证用户名和密码
if (username == request.form.get("username")) and (
password == request.form.get("password")
):
if not input_username or not input_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} 登录成功")
session.permanent = True
session["token"] = get_login_token()
return redirect(url_for("index"))
else:
logging.info(f">>> 用户 {username} 登录失败")
return render_template("login.html", message="登录失败")
if is_login():
return redirect(url_for("index"))
@ -154,7 +175,11 @@ def get_data():
if not is_login():
return jsonify({"success": False, "message": "未登录"})
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["task_plugins_config_default"] = task_plugins_config_default
return jsonify({"success": True, "data": data})
@ -169,8 +194,15 @@ def update():
dont_save_keys = ["task_plugins_config_default", "api_token"]
for key, value in request.json.items():
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)
# 更新session token确保当前会话在用户名密码更改后仍然有效
session["token"] = get_login_token()
# 重新加载任务
if reload_tasks():
logging.info(f">>> 配置更新成功")
@ -197,6 +229,9 @@ def run_script_now():
process_env["PYTHONIOENCODING"] = "utf-8"
if tasklist:
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(
command,
stdout=subprocess.PIPE,
@ -294,15 +329,23 @@ def is_date_format(number_str):
return False
@app.route("/get_share_detail", methods=["POST"])
# 获取分享详情接口
@app.route("/get_share_detail", methods=["GET", "POST"])
def get_share_detail():
if not is_login():
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的必要属性
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)
if not stoken:
@ -313,6 +356,10 @@ def get_share_detail():
share_detail["paths"] = paths
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):
regex = request.json.get("regex")
@ -408,7 +455,6 @@ def get_share_detail():
regex_pattern = re.escape(episode_pattern).replace('\\[\\]', '(\\d+)')
else:
# 如果输入模式不包含[],则使用简单匹配模式,避免正则表达式错误
print(f"⚠️ 剧集命名模式中没有找到 [] 占位符,将使用简单匹配")
regex_pattern = "^" + re.escape(episode_pattern) + "(\\d+)$"
# 实现高级排序算法
@ -643,7 +689,7 @@ def init():
"username": os.environ.get("WEBUI_USERNAME")
or config_data.get("webui", {}).get("username", "admin"),
"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)
# 获取历史转存记录
@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__":
init()
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>
<meta charset="UTF-8">
<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-icons.min.css">
<style>
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>
<link rel="stylesheet" href="./static/css/main.css">
</head>
<body>
<div class="container">
<div class="login-card">
<div class="login-header">
<h1 class="mb-3">登录</h1>
<p class="text-muted">欢迎回来,请登录您的账户</p>
<body class="login-page">
<div class="login-card">
<div class="login-header">
<h1 class="login-title"><a href="https://github.com/x1ao4/quark-auto-save-x" target="_blank">夸克自动转存</a></h1>
<p class="login-subtitle">欢迎回来,请登录您的账号</p>
</div>
<div class="login-body">
{% if message %}
<div class="alert" role="alert">
[[ message ]]
</div>
<div class="login-body">
{% if message %}
<div class="alert alert-danger text-center" role="alert">
[[ message ]]
{% endif %}
<form action="/login" method="POST">
<div class="form-group">
<input type="text" class="form-control" id="username" name="username" placeholder="用户名" required>
</div>
{% endif %}
<form action="/login" method="POST">
<div class="form-group mb-3">
<label for="username" class="form-label">用户名</label>
<div class="input-group">
<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 class="form-group">
<input type="password" class="form-control" id="password" name="password" placeholder="密码" required>
</div>
<button type="submit" class="btn">登录</button>
</form>
</div>
</div>
</body>

View File

@ -1,5 +1,22 @@
import os
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:
@ -35,36 +52,149 @@ class Aria2:
)
if not task_config.get("auto_download"):
return
if (tree := kwargs.get("tree")) and (account := kwargs.get("account")):
# 按文件路径排序添加下载任务
nodes = sorted(
tree.all_nodes_itr(), key=lambda node: node.data.get("path", "")
)
file_fids = []
file_paths = []
for node in nodes:
if not node.data.get("is_dir", True):
file_fids.append(node.data.get("fid"))
file_paths.append(node.data.get("path"))
download_return, cookie = account.download(file_fids)
file_urls = [item["download_url"] for item in download_return["data"]]
for index, file_url in enumerate(file_urls):
file_path = file_paths[index]
print(f"📥 Aria2下载: {file_path}")
local_path = f"{self.dir}{file_paths[index]}"
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"),
},
]
account = kwargs.get("account")
if not account:
return
# 获取重命名日志,优先使用传入的参数
rename_logs = kwargs.get("rename_logs", [])
# 从重命名日志中提取文件信息
renamed_files = {}
for log in rename_logs:
if "重命名:" in log and "" in log:
# 精确匹配分割
parts = log.split("重命名:", 1)[1].strip()
if "" in parts:
old_name, new_name = parts.split("", 1)
# 排除失败信息
if " 失败," in new_name:
new_name = new_name.split(" 失败,")[0]
# 清理空白
old_name = old_name.strip()
new_name = new_name.strip()
renamed_files[old_name] = new_name
# 获取文件树,确定本次需要下载的文件
current_files_to_download = set()
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)
except Exception as e:
print(f"📥 Aria2添加下载任务失败: {e}")
def _make_rpc_request(self, method, params=None):
"""发出 JSON-RPC 请求."""

View File

@ -1,7 +1,5 @@
# !/usr/bin/env python3
# -*- coding: utf-8 -*-
# Modify: 2024-11-13
# Repo: https://github.com/Cp0204/quark_auto_save
# ConfigFile: quark_config.json
"""
new Env('夸克自动追更');
@ -18,6 +16,26 @@ import importlib
import urllib.parse
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):
"""
@ -425,6 +443,29 @@ def add_notify(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):
"""根据文件扩展名返回对应的图标"""
@ -987,8 +1028,15 @@ class Quark:
<= datetime.strptime(item["enddate"], "%Y-%m-%d").date()
)
]
# 去掉每个路径开头的斜杠,确保格式一致
dir_paths = [path.lstrip('/') for path in dir_paths]
if not dir_paths:
return False
# 重新添加斜杠前缀,确保格式一致
dir_paths = [f"/{path}" for path in dir_paths]
dir_paths_exist_arr = self.get_fids(dir_paths)
dir_paths_exist = [item["file_path"] for item in dir_paths_exist_arr]
# 比较创建不存在的
@ -1050,12 +1098,201 @@ class Quark:
except Exception as 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):
# 判断资源失效记录
if task.get("shareurl_ban"):
print(f"分享资源已失效:{task['shareurl_ban']}")
add_notify(f"❗《{task['taskname']}》分享资源已失效:{task['shareurl_ban']}\n")
return
# 标准化保存路径,去掉可能存在的首位斜杠,然后重新添加
savepath = task["savepath"].lstrip('/')
task["savepath"] = savepath # 更新任务中的路径,确保后续处理一致
# 提取链接参数
pwd_id, passcode, pdir_fid, paths = self.extract_url(task["shareurl"])
if not pwd_id:
@ -1080,13 +1317,13 @@ class Quark:
else:
savepath_fids = self.get_fids([savepath])
if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}")
# print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}")
# print(f"保存路径新建成功:{savepath}")
else:
print(f"保存路径新建失败:{mkdir_result['message']}")
# print(f"保存路径新建失败:{mkdir_result['message']}")
return
else:
# 路径已存在直接设置fid
@ -1254,13 +1491,13 @@ class Quark:
else:
savepath_fids = self.get_fids([savepath])
if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}")
# print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}")
# print(f"保存路径新建成功:{savepath}")
else:
print(f"保存路径新建失败:{mkdir_result['message']}")
# print(f"保存路径新建失败:{mkdir_result['message']}")
return
else:
# 路径已存在直接设置fid
@ -1479,7 +1716,7 @@ class Quark:
# 合并子目录树
tree.create_node(
"📁" + share_file["file_name"],
f"📁{share_file['file_name']}",
share_file["fid"],
parent=pdir_fid,
data={
@ -1668,7 +1905,7 @@ class Quark:
# 添加目录到树中但不添加到保存列表
if not tree.contains(share_file["fid"]):
tree.create_node(
"📁" + share_file["file_name"],
f"📁{share_file['file_name']}",
share_file["fid"],
parent=pdir_fid,
data={
@ -1825,7 +2062,7 @@ class Quark:
# 检查节点是否已存在于树中,避免重复添加
if not tree.contains(share_file["fid"]):
tree.create_node(
"📁" + share_file["file_name"],
f"📁{share_file['file_name']}",
share_file["fid"],
parent=pdir_fid,
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"]):
tree.create_node(
@ -1961,6 +2198,14 @@ class Quark:
"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函数统一处理
# 顺序命名模式和剧集命名模式都不在此处生成通知
@ -2108,6 +2353,14 @@ class Quark:
# 移除直接打印的部分由do_save负责打印
# print(rename_log)
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:
error_msg = rename_return.get("message", "未知错误")
rename_log = f"重命名: {dir_file['file_name']}{save_name} 失败,{error_msg}"
@ -2134,13 +2387,13 @@ class Quark:
# 路径已存在直接设置fid
savepath_fids = self.get_fids([savepath])
if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}")
# print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}")
# print(f"保存路径新建成功:{savepath}")
else:
print(f"保存路径新建失败:{mkdir_result['message']}")
# print(f"保存路径新建失败:{mkdir_result['message']}")
return False, []
else:
self.savepath_fid[savepath] = savepath_fids[0]["fid"]
@ -2383,6 +2636,15 @@ class Quark:
# 进行重命名操作,确保文件按照预览名称保存
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])
@ -2479,6 +2741,13 @@ class Quark:
if df["fid"] == dir_file["fid"]:
df["file_name"] = target_name
break
# 不在这里直接调用update_transfer_record而是在do_save中统一处理
# self.update_transfer_record(
# task=task,
# file_info=dir_file,
# renamed_to=target_name
# )
else:
# 收集错误日志但不打印
error_log = f"重命名: {dir_file['file_name']}{target_name} 失败,{rename_result['message']}"
@ -2504,7 +2773,7 @@ class Quark:
# 对本地已有文件进行重命名(即使没有分享链接或处理失败也执行)
is_rename_count = 0
renamed_files = []
renamed_files = {}
# 使用一个列表收集所有需要重命名的操作
rename_operations = []
@ -2552,6 +2821,15 @@ class Quark:
if df["fid"] == dir_file["fid"]:
df["file_name"] = new_name
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:
# 收集错误日志但不打印
error_msg = rename_return.get("message", "未知错误")
@ -2617,13 +2895,13 @@ class Quark:
# 路径不存在创建或获取fid
savepath_fids = self.get_fids([savepath])
if not savepath_fids:
print(f"保存路径不存在,准备新建:{savepath}")
# print(f"保存路径不存在,准备新建:{savepath}")
mkdir_result = self.mkdir(savepath)
if mkdir_result["code"] == 0:
self.savepath_fid[savepath] = mkdir_result["data"]["fid"]
print(f"保存路径新建成功:{savepath}")
# print(f"保存路径新建成功:{savepath}")
else:
print(f"保存路径新建失败:{mkdir_result['message']}")
# print(f"保存路径新建失败:{mkdir_result['message']}")
return False, []
else:
self.savepath_fid[savepath] = savepath_fids[0]["fid"]
@ -2807,8 +3085,17 @@ def do_save(account, 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(f"#{index+1}------------------")
print(f"#{str(display_index).zfill(2)}------------------")
print(f"任务名称: {task['taskname']}")
print(f"分享链接: {task['shareurl']}")
print(f"保存路径: {task['savepath']}")
@ -2945,10 +3232,10 @@ def do_save(account, tasklist=[]):
renamed_files = {}
for log in rename_logs:
# 格式:重命名: 旧名 → 新名
match = re.search(r'重命名: (.*?) → (.*?)($|\s|)', log)
match = re.search(r'重命名: (.*?) → (.+?)($|\s||失败)', log)
if match:
old_name = match.group(1)
new_name = match.group(2)
old_name = match.group(1).strip()
new_name = match.group(2).strip()
renamed_files[old_name] = new_name
# 获取文件列表,只添加重命名的文件
@ -3016,10 +3303,10 @@ def do_save(account, tasklist=[]):
renamed_files = {}
for log in rename_logs:
# 格式:重命名: 旧名 → 新名
match = re.search(r'重命名: (.*?) → (.*?)($|\s|)', log)
match = re.search(r'重命名: (.*?) → (.+?)($|\s||失败)', log)
if match:
old_name = match.group(1)
new_name = match.group(2)
old_name = match.group(1).strip()
new_name = match.group(2).strip()
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))
# 添加到显示列表
display_files.append((f"{icon}{new_filename}", node))
display_files.append((f"{icon} {new_filename}", node))
else:
# 如果没有重命名日志,使用原来的顺序命名逻辑
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))
# 添加到显示列表
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'))
@ -3066,10 +3353,10 @@ def do_save(account, tasklist=[]):
renamed_files = {}
for log in rename_logs:
# 格式:重命名: 旧名 → 新名
match = re.search(r'重命名: (.*?) → (.*?)($|\s|)', log)
match = re.search(r'重命名: (.*?) → (.+?)($|\s||失败)', log)
if match:
old_name = match.group(1)
new_name = match.group(2)
old_name = match.group(1).strip()
new_name = match.group(2).strip()
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))
# 添加到显示列表
display_files.append((f"{icon}{new_filename}", node))
display_files.append((f"{icon} {new_filename}", node))
# 如果没有找到任何文件要显示,使用原始文件名
if not display_files:
@ -3100,7 +3387,7 @@ def do_save(account, tasklist=[]):
orig_filename = node.tag.lstrip("🎞️")
# 添加适当的图标
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:
# 其他模式:显示原始文件名
display_files = []
@ -3224,7 +3511,7 @@ def do_save(account, tasklist=[]):
# 添加文件节点到结构中
dir_structure[parent_id].append({
"id": node.identifier,
"name": f"{icon}{orig_filename}",
"name": f"{icon} {orig_filename}",
"is_dir": False
})
@ -3513,7 +3800,7 @@ def do_save(account, tasklist=[]):
else:
# 添加基本通知
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:
@ -3649,7 +3936,7 @@ def do_save(account, tasklist=[]):
dir_name != save_path_basename):
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_name = file_node.tag.lstrip("🎞️")
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:
@ -3710,9 +3997,10 @@ def do_save(account, tasklist=[]):
# 再按箭头分割
if "" in parts:
old_name, new_name = parts.split("", 1)
# 如果新名称包含空格或其他分隔符,只取第一个换行符之前的内容
if "\n" in new_name:
new_name = new_name.split("\n")[0]
# 只处理失败信息,不截断正常文件名
if " 失败," in new_name:
new_name = new_name.split(" 失败,")[0]
# 去除首尾空格
old_name = old_name.strip()
new_name = new_name.strip()
renamed_files[old_name] = new_name
@ -3794,7 +4082,7 @@ def do_save(account, tasklist=[]):
# 添加成功通知 - 修复问题:确保在有文件时添加通知
if display_files:
add_notify(f"✅《{task['taskname']}》添加追更:")
add_notify(f"/{task['savepath']}")
add_notify(f"{re.sub(r'/{2,}', '/', f'/{task['savepath']}')}")
# 创建episode_pattern函数用于排序
@ -3834,7 +4122,7 @@ def do_save(account, tasklist=[]):
icon = get_file_icon(file_name, is_dir=False)
else:
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:
@ -3852,10 +4140,10 @@ def do_save(account, tasklist=[]):
renamed_files = {}
for log in rename_logs:
# 格式:重命名: 旧名 → 新名
match = re.search(r'重命名: (.*?) → (.*?)($|\s|)', log)
match = re.search(r'重命名: (.*?) → (.+?)($|\s||失败)', log)
if match:
old_name = match.group(1)
new_name = match.group(2)
old_name = match.group(1).strip()
new_name = match.group(2).strip()
renamed_files[old_name] = new_name
# 只显示重命名的文件
@ -3871,48 +4159,48 @@ def do_save(account, tasklist=[]):
# 添加成功通知
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):
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)]
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("")
# 打印重命名日志(文件树之后)
if rename_logs:
# 处理重命名日志,更新数据库记录
account.process_rename_logs(task, rename_logs)
# 对剧集命名模式和其他模式统一处理重命名日志
# 按剧集号/顺序号排序重命名日志
# 按sort_file_by_name函数的多级排序逻辑排序重命名日志
sorted_rename_logs = []
for log in rename_logs:
# 提取新文件名(格式:重命名: 旧名 → 新名)
match = re.search(r'\s+(.+?)($|\s|)', log)
if match:
new_name = match.group(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))
# 使用更精确的字符串分割方法,确保能捕获完整的文件名
if "重命名:" in log and "" in log:
parts = log.split("重命名:", 1)[1].strip()
if "" in parts:
_, new_name = parts.split("", 1)
# 只处理失败信息,不截断正常文件名
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((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])
# 打印排序后的日志
@ -3949,7 +4237,7 @@ def do_save(account, tasklist=[]):
for plugin_name, plugin in plugins.items():
if plugin.is_active and (is_new_tree or is_rename):
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: # 明确没有新文件
print(f"任务完成: 没有新的文件需要转存")