在任务列表页面新增显示最近转存文件功能并优化 API 性能

- 在任务名称后显示最新转存文件名(格式:· 文件名)
- 新增显示设置选项:始终显示/悬停显示/禁用
- 合并 /task_latest_records 和 /task_latest_files 为统一的 /task_latest_info API
- 自动提取季数集数信息显示(如:乘风2025 - S06E10 → S06E10)
- 样式与最近更新日期保持一致,支持悬停交互
- 完全向后兼容,不影响现有功能和配置
This commit is contained in:
x1ao4 2025-07-05 02:58:29 +08:00
parent 08e5a2f6e8
commit d61c0ee9cd
3 changed files with 139 additions and 29 deletions

View File

@ -40,6 +40,35 @@ from quark_auto_save import Config, format_bytes
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, chinese_to_arabic, is_date_format
def process_season_episode_info(filename):
"""
处理文件名中的季数和集数信息
Args:
filename: 文件名不含扩展名
Returns:
处理后的显示名称
"""
# 匹配 SxxExx 格式(不区分大小写)
# 支持 S1E1, S01E01, s13e10 等格式
season_episode_match = re.search(r'[Ss](\d{1,2})[Ee](\d{1,3})', filename)
if season_episode_match:
season = season_episode_match.group(1).zfill(2) # 确保两位数
episode = season_episode_match.group(2).zfill(2) # 确保两位数
return f"S{season}E{episode}"
# 匹配只有 Exx 或 EPxx 格式(不区分大小写)
# 支持 E1, E01, EP1, EP01, e10, ep10 等格式
episode_only_match = re.search(r'[Ee][Pp]?(\d{1,3})', filename)
if episode_only_match:
episode = episode_only_match.group(1).zfill(2) # 确保两位数
return f"E{episode}"
# 如果没有匹配到季数集数信息,返回原文件名
return filename
# 导入拼音排序工具
try:
from utils.pinyin_sort import get_filename_pinyin_sort_key
@ -1364,18 +1393,18 @@ def delete_history_records():
})
# 获取任务最新转存记录日期
@app.route("/task_latest_records")
def get_task_latest_records():
# 获取任务最新转存信息(包括日期和文件)
@app.route("/task_latest_info")
def get_task_latest_info():
if not is_login():
return jsonify({"success": False, "message": "未登录"})
try:
# 初始化数据库
db = RecordDB()
# 获取所有任务的最新转存记录
cursor = db.conn.cursor()
# 获取所有任务的最新转存时间
query = """
SELECT task_name, MAX(transfer_time) as latest_transfer_time
FROM transfer_records
@ -1383,13 +1412,14 @@ def get_task_latest_records():
GROUP BY task_name
"""
cursor.execute(query)
results = cursor.fetchall()
latest_times = cursor.fetchall()
# 转换为字典格式
task_latest_records = {}
for row in results:
task_name, latest_time = row
task_latest_records = {} # 存储最新转存日期
task_latest_files = {} # 存储最新转存文件
for task_name, latest_time in latest_times:
if latest_time:
# 1. 处理最新转存日期
try:
# 确保时间戳在合理范围内
timestamp = int(latest_time)
@ -1404,20 +1434,67 @@ def get_task_latest_records():
except (ValueError, TypeError, OverflowError):
pass # 忽略无效的时间戳
# 2. 处理最新转存文件
# 获取该任务在最新转存时间附近(同一分钟内)的所有文件
# 这样可以处理同时转存多个文件但时间戳略有差异的情况
time_window = 60000 # 60秒的时间窗口毫秒
query = """
SELECT renamed_to, original_name, transfer_time, modify_date
FROM transfer_records
WHERE task_name = ? AND transfer_time >= ? AND transfer_time <= ?
ORDER BY id DESC
"""
cursor.execute(query, (task_name, latest_time - time_window, latest_time + time_window))
files = cursor.fetchall()
if files:
if len(files) == 1:
# 如果只有一个文件,直接使用
best_file = files[0][0] # renamed_to
else:
# 如果有多个文件,使用全局排序函数进行排序
file_list = []
for renamed_to, original_name, transfer_time, modify_date in files:
# 构造文件信息字典,模拟全局排序函数需要的格式
file_info = {
'file_name': renamed_to,
'original_name': original_name,
'updated_at': transfer_time # 使用转存时间而不是文件修改时间
}
file_list.append(file_info)
# 使用全局排序函数进行正向排序,最后一个就是最新的
try:
sorted_files = sorted(file_list, key=sort_file_by_name)
best_file = sorted_files[-1]['file_name'] # 取排序后的最后一个文件
except Exception as e:
# 如果排序失败,使用第一个文件作为备选
best_file = files[0][0]
# 去除扩展名并处理季数集数信息
if best_file:
file_name_without_ext = os.path.splitext(best_file)[0]
processed_name = process_season_episode_info(file_name_without_ext)
task_latest_files[task_name] = processed_name
db.close()
return jsonify({
"success": True,
"data": task_latest_records
"data": {
"latest_records": task_latest_records,
"latest_files": task_latest_files
}
})
except Exception as e:
return jsonify({
"success": False,
"message": f"获取任务最新记录失败: {str(e)}"
"message": f"获取任务最新信息失败: {str(e)}"
})
# 删除单条转存记录
@app.route("/delete_history_record", methods=["POST"])
def delete_history_record():

View File

@ -4060,17 +4060,25 @@ table.selectable-records .expand-button:hover {
color: var(--dark-text-color);
font-size: 0.95rem;
font-weight: normal;
opacity: 1;
transition: opacity 0.2s ease;
}
/* 悬停显示模式 */
.task .btn:not(:hover) .task-latest-date.hover-only {
opacity: 0;
/* 任务最近转存文件样式 */
.task-latest-file {
color: var(--dark-text-color);
font-size: 0.95rem;
font-weight: normal;
}
.task .btn:hover .task-latest-date.hover-only {
opacity: 1;
/* 悬停显示模式 - 使用display来避免占用空间同时保持平滑的悬停效果 */
.task-latest-date.hover-only,
.task-latest-file.hover-only {
display: none;
transition: all 0.2s ease;
}
.task .btn:hover .task-latest-date.hover-only,
.task .btn:hover .task-latest-file.hover-only {
display: inline;
}
/* 显示设置行样式 */

View File

@ -718,6 +718,18 @@
</select>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-2">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">最近转存文件</span>
</div>
<select class="form-control" v-model="formData.button_display.latest_transfer_file">
<option value="always">始终显示</option>
<option value="hover">悬停显示</option>
<option value="disabled">禁用</option>
</select>
</div>
</div>
</div>
<!-- 性能设置 -->
@ -800,6 +812,11 @@
:class="{'hover-only': formData.button_display.latest_update_date === 'hover'}">
· {{ taskLatestRecords[task.taskname] }}
</span>
<span v-if="formData.button_display.latest_transfer_file !== 'disabled' && taskLatestFiles[task.taskname]"
class="task-latest-file"
:class="{'hover-only': formData.button_display.latest_transfer_file === 'hover'}">
· {{ taskLatestFiles[task.taskname] }}
</span>
</div>
</div>
<div class="col-auto task-buttons">
@ -1633,7 +1650,8 @@
delete_task: "always",
refresh_plex: "always",
refresh_alist: "always",
latest_update_date: "always"
latest_update_date: "always",
latest_transfer_file: "always"
},
file_performance: {
api_page_size: 200,
@ -1663,6 +1681,7 @@
taskDirSelected: "",
taskNameFilter: "",
taskLatestRecords: {}, // 存储每个任务的最新转存记录日期
taskLatestFiles: {}, // 存储每个任务的最近转存文件
modalLoading: false,
smart_param: {
index: null,
@ -2560,13 +2579,18 @@
delete_task: "always",
refresh_plex: "always",
refresh_alist: "always",
latest_update_date: "always"
latest_update_date: "always",
latest_transfer_file: "always"
};
}
// 确保最近更新日期配置存在(向后兼容)
if (!config_data.button_display.latest_update_date) {
config_data.button_display.latest_update_date = "always";
}
// 确保最近转存文件配置存在(向后兼容)
if (!config_data.button_display.latest_transfer_file) {
config_data.button_display.latest_transfer_file = "always";
}
// 确保文件整理性能配置存在
if (!config_data.file_performance) {
config_data.file_performance = {
@ -2590,8 +2614,8 @@
this.configModified = false;
}, 100);
// 加载任务最新记录
this.loadTaskLatestRecords();
// 加载任务最新信息(包括记录和文件)
this.loadTaskLatestInfo();
// 数据加载完成后检查分享链接状态
if (this.activeTab === 'tasklist') {
@ -4167,18 +4191,19 @@
}
});
},
loadTaskLatestRecords() {
// 获取所有任务的最新转存记录日期
axios.get('/task_latest_records')
loadTaskLatestInfo() {
// 获取所有任务的最新转存信息(包括日期和文件)
axios.get('/task_latest_info')
.then(response => {
if (response.data.success) {
this.taskLatestRecords = response.data.data;
this.taskLatestRecords = response.data.data.latest_records;
this.taskLatestFiles = response.data.data.latest_files;
} else {
console.error('获取任务最新记录失败:', response.data.message);
console.error('获取任务最新信息失败:', response.data.message);
}
})
.catch(error => {
console.error('获取任务最新记录失败:', error);
console.error('获取任务最新信息失败:', error);
});
},
openDatePicker(index) {