diff --git a/app/run.py b/app/run.py index 02d2214..f4b16bd 100644 --- a/app/run.py +++ b/app/run.py @@ -27,6 +27,7 @@ import os import re import random import time +import treelib parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, parent_dir) @@ -35,19 +36,41 @@ 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 +from quark_auto_save import extract_episode_number, sort_file_by_name, chinese_to_arabic, is_date_format # 导入数据库模块 try: - from app.sdk.db import RecordDB + # 先尝试相对导入 + from 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}} + try: + # 如果相对导入失败,尝试从app包导入 + 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}} + +# 导入工具函数 +try: + # 先尝试相对导入 + from sdk.utils import format_bytes, get_file_icon, format_file_display +except ImportError: + try: + # 如果相对导入失败,尝试从app包导入 + from app.sdk.utils import format_bytes, get_file_icon, format_file_display + except ImportError: + # 如果导入失败,使用默认实现或从quark_auto_save导入 + # format_bytes已从quark_auto_save导入 + def get_file_icon(file_name, is_dir=False): + return "📄" if not is_dir else "📁" + + def format_file_display(prefix, icon, name): + return f"{prefix}{icon} {name}" def get_app_ver(): @@ -463,37 +486,6 @@ def get_task_suggestions(): return jsonify({"success": True, "message": f"error: {str(e)}"}) -# 添加函数,与主程序保持一致 -def is_date_format(number_str): - """ - 判断一个纯数字字符串是否可能是日期格式 - 支持的格式:YYYYMMDD, MMDD - """ - # 判断YYYYMMDD格式 (8位数字) - if len(number_str) == 8 and number_str.startswith('20'): - year = int(number_str[:4]) - month = int(number_str[4:6]) - day = int(number_str[6:8]) - - # 简单检查月份和日期是否有效 - if 1 <= month <= 12 and 1 <= day <= 31: - # 可能是日期格式 - return True - - # 判断MMDD格式 (4位数字) - elif len(number_str) == 4: - month = int(number_str[:2]) - day = int(number_str[2:4]) - - # 简单检查月份和日期是否有效 - if 1 <= month <= 12 and 1 <= day <= 31: - # 可能是日期格式 - return True - - # 其他长度的纯数字不视为日期格式 - return False - - # 获取分享详情接口 @app.route("/get_share_detail", methods=["GET", "POST"]) def get_share_detail(): @@ -599,6 +591,22 @@ def get_share_detail(): episode_pattern = regex.get("episode_naming") episode_patterns = regex.get("episode_patterns", []) + # 添加中文数字匹配模式 + chinese_patterns = [ + {"regex": r'第([一二三四五六七八九十百千万零两]+)集'}, + {"regex": r'第([一二三四五六七八九十百千万零两]+)期'}, + {"regex": r'第([一二三四五六七八九十百千万零两]+)话'}, + {"regex": r'([一二三四五六七八九十百千万零两]+)集'}, + {"regex": r'([一二三四五六七八九十百千万零两]+)期'}, + {"regex": r'([一二三四五六七八九十百千万零两]+)话'} + ] + + # 合并中文模式到episode_patterns + if episode_patterns: + episode_patterns.extend(chinese_patterns) + else: + episode_patterns = chinese_patterns + # 调用全局的集编号提取函数 def extract_episode_number_local(filename): return extract_episode_number(filename, episode_patterns=episode_patterns) @@ -672,7 +680,7 @@ def get_share_detail(): if any(word in item['file_name'] for word in filterwords_list): item["filtered"] = True - # 为每个文件生成新文件名 + # 为每个文件生成新文件名并存储剧集编号用于排序 for file in sorted_files: if not file.get("filtered"): # 获取文件扩展名 @@ -686,9 +694,12 @@ def get_share_detail(): file["file_name_re"] = f"{episode_num:02d}{file_ext}" else: file["file_name_re"] = episode_pattern.replace("[]", f"{episode_num:02d}") + file_ext + # 存储原始的剧集编号,用于数值排序 + file["episode_number"] = episode_num else: # 无法提取剧集号,标记为无法处理 file["file_name_re"] = "❌ 无法识别剧集号" + file["episode_number"] = 9999999 # 给一个很大的值,确保排在最后 return share_detail else: diff --git a/app/static/css/main.css b/app/static/css/main.css index b41152e..8311f99 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -849,6 +849,8 @@ select.form-control { box-sizing: border-box !important; border-top-left-radius: 0 !important; /* 移除左上角圆角 */ border-bottom-left-radius: 0 !important; /* 移除左下角圆角 */ + border-top-right-radius: 0 !important; /* 移除右上角圆角 */ + border-bottom-right-radius: 0 !important; /* 移除右下角圆角 */ } /* 为确保输入组中select元素的边角正确 */ @@ -4099,3 +4101,31 @@ table.selectable-records .expand-button:hover { .task:hover .btn.btn-block.text-left { color: var(--focus-border-color); } + +select.task-filter-select, +.task-filter-select { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.input-group .form-control:focus { + z-index: 3; + position: relative; +} +.input-group .form-control:focus + .input-group-append .btn, +.input-group .form-control:focus + .input-group-append .btn:focus { + border-left-color: #2563eb !important; /* 激活时的边框色 */ + box-shadow: none !important; + position: relative; + z-index: 2; +} + +/* 添加任务名称悬停样式 */ +.task-name-hover { + cursor: pointer; + transition: color 0.2s ease-in-out; +} + +.task-name-hover:hover { + color: var(--focus-border-color) !important; +} diff --git a/app/templates/index.html b/app/templates/index.html index a1b6fa0..13a4436 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -115,6 +115,66 @@ } } }); + + // 添加中文数字转阿拉伯数字的函数 + function chineseToArabic(chinese) { + if (!chinese) { + return null; + } + + // 数字映射 + const digitMap = { + '零': 0, '一': 1, '二': 2, '三': 3, '四': 4, + '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, + '两': 2 + }; + + // 单位映射 + const unitMap = { + '十': 10, + '百': 100, + '千': 1000, + '万': 10000 + }; + + // 如果是单个字符,直接返回对应数字 + if (chinese.length === 1) { + if (chinese === '十') return 10; + return digitMap[chinese]; + } + + let result = 0; + let section = 0; + let number = 0; + + // 从左向右处理 + for (let i = 0; i < chinese.length; i++) { + const char = chinese[i]; + + if (char in digitMap) { + number = digitMap[char]; + } else if (char in unitMap) { + const unit = unitMap[char]; + // 如果前面没有数字,默认为1,例如"十"表示1*10=10 + section += (number || 1) * unit; + number = 0; + + // 如果是万级单位,累加到结果并重置section + if (unit === 10000) { + result += section; + section = 0; + } + } else { + // 非法字符 + return null; + } + } + + // 加上最后的数字和小节 + result += section + number; + + return result; + } @@ -523,7 +583,7 @@ - +
@@ -735,7 +795,7 @@ - +
@@ -768,13 +828,15 @@
+ v-check-overflow="index + '|task_name'" + :class="{'task-name-hover': true}" + @click.stop="filterByTaskName(record.task_name, $event)"> {{ record.task_name }}
-
+
{{ record.task_name }}
@@ -1788,13 +1850,10 @@ if (match) { separatorMatch = match; // 根据不同的格式,确定季序号的位置 - if (match[3] && match[3].match(/[一二三四五六七八九十零]/)) { + if (match[3] && match[3].match(/[一二三四五六七八九十百千万零两]/)) { // 将中文数字转换为阿拉伯数字 - const chineseToNumber = { - '零': '0', '一': '1', '二': '2', '三': '3', '四': '4', - '五': '5', '六': '6', '七': '7', '八': '8', '九': '9', '十': '10' - }; - seasonNumber = chineseToNumber[match[3]] || '01'; + const arabicNumber = chineseToArabic(match[3]); + seasonNumber = arabicNumber !== null ? String(arabicNumber) : '01'; } else { seasonNumber = match[3] || '01'; } @@ -1891,18 +1950,15 @@ if (pathParts.length > 0) { const lastPart = pathParts[pathParts.length - 1]; // 匹配中文季序号格式 - const chineseSeasonMatch = lastPart.match(/^(.*?)([\s\.\-_]+)第([一二三四五六七八九十零]+)季$/); + const chineseSeasonMatch = lastPart.match(/^(.*?)([\s\.\-_]+)第([一二三四五六七八九十百千万零两]+)季$/); if (chineseSeasonMatch) { const showName = chineseSeasonMatch[1].trim(); const separator = chineseSeasonMatch[2]; const chineseSeason = chineseSeasonMatch[3]; // 将中文数字转换为阿拉伯数字 - const chineseToNumber = { - '零': '0', '一': '1', '二': '2', '三': '3', '四': '4', - '五': '5', '六': '6', '七': '7', '八': '8', '九': '9', '十': '10' - }; - const seasonNumber = chineseToNumber[chineseSeason] || '1'; + const arabicNumber = chineseToArabic(chineseSeason); + const seasonNumber = arabicNumber !== null ? String(arabicNumber) : '1'; // 更新最末级目录为标准格式 pathParts[pathParts.length - 1] = showName + separator + 'S' + seasonNumber.padStart(2, '0'); @@ -2345,6 +2401,9 @@ }, selectSuggestion(index, suggestion) { this.smart_param.showSuggestions = false; + // 确保显示的是选择需转存的文件夹界面,而不是命名预览界面 + this.fileSelect.previewRegex = false; + this.fileSelect.selectDir = true; this.showShareSelect(index, suggestion.shareurl); }, addMagicRegex() { @@ -2419,7 +2478,7 @@ alert('删除项目出错: ' + (error.response?.data?.message || error.message)); }); }, - getSavepathDetail(params = 0) { + getSavepathDetail(params = 0, retryCount = 0, maxRetries = 3) { if (params === "" || params === null || params === undefined) { // 为空字符串时直接使用根目录fid params = 0; @@ -2440,8 +2499,18 @@ } this.modalLoading = false; }).catch(error => { - this.fileSelect.error = "获取文件夹列表失败"; - this.modalLoading = false; + // 如果还有重试次数,则进行重试 + if (retryCount < maxRetries) { + console.log(`获取文件夹列表失败,正在进行第 ${retryCount + 1} 次重试...`); + // 短暂延迟后重试 + setTimeout(() => { + this.getSavepathDetail(params, retryCount + 1, maxRetries); + }, 1000); // 1秒后重试 + } else { + // 超过最大重试次数,显示错误信息 + this.fileSelect.error = "获取文件夹列表失败,请关闭窗口再试一次"; + this.modalLoading = false; + } }); }, showSavepathSelect(index) { @@ -2469,7 +2538,7 @@ this.getSavepathDetail(savepath); } }, - getShareDetail() { + getShareDetail(retryCount = 0, maxRetries = 3) { this.modalLoading = true; axios.post('/get_share_detail', { shareurl: this.fileSelect.shareurl, @@ -2515,8 +2584,18 @@ } this.modalLoading = false; }).catch(error => { - this.fileSelect.error = "获取文件夹列表失败"; - this.modalLoading = false; + // 如果还有重试次数,则进行重试 + if (retryCount < maxRetries) { + console.log(`获取文件夹列表失败,正在进行第 ${retryCount + 1} 次重试...`); + // 短暂延迟后重试 + setTimeout(() => { + this.getShareDetail(retryCount + 1, maxRetries); + }, 1000); // 1秒后重试 + } else { + // 超过最大重试次数,显示错误信息 + this.fileSelect.error = "获取文件夹列表失败,请关闭窗口再试一次"; + this.modalLoading = false; + } }); }, showShareSelect(index, shareurl = null) { @@ -2550,7 +2629,8 @@ } $('#fileSelectModal').modal('toggle'); - this.getShareDetail(); + // 调用getShareDetail时不传递任何参数,使用默认的重试机制 + this.getShareDetail(0, 3); // 命名预览模式下,确保在模态框显示后检查滚动条状态 if (this.fileSelect.previewRegex) { @@ -2563,7 +2643,8 @@ path = { fid: fid, name: name } if (this.fileSelect.selectShare) { this.fileSelect.shareurl = this.getShareurl(this.fileSelect.shareurl, path); - this.getShareDetail(); + // 使用重试机制调用getShareDetail + this.getShareDetail(0, 3); } else { if (fid == "0") { this.fileSelect.paths = [] @@ -2575,7 +2656,8 @@ this.fileSelect.paths.push({ fid: fid, name: name }) } } - this.getSavepathDetail(fid); + // 使用重试机制调用getSavepathDetail + this.getSavepathDetail(fid, 0, 3); } }, selectCurrentFolder(addTaskname = false) { @@ -3156,14 +3238,13 @@ }, // 文件选择模态框的排序方法 sortFileList(field) { - // 如果点击了当前排序字段,则切换排序顺序 + // 切换排序方向 if (this.fileSelect.sortBy === field) { this.fileSelect.sortOrder = this.fileSelect.sortOrder === 'asc' ? 'desc' : 'asc'; } else { - // 如果点击了不同的字段,则设置为该字段并使用默认排序顺序 this.fileSelect.sortBy = field; - // 对于日期和重命名列使用降序,其他字段使用升序 - this.fileSelect.sortOrder = field === 'updated_at' || field === 'file_name_re' ? 'desc' : 'asc'; + // 默认降序(除了文件名外) + this.fileSelect.sortOrder = field === 'file_name' ? 'asc' : 'desc'; } // 按选定字段和顺序对文件列表进行排序 @@ -3179,9 +3260,15 @@ aValue = a.file_name.toLowerCase(); bValue = b.file_name.toLowerCase(); } else if (field === 'file_name_re') { - // 对于重命名列,使用重命名后的文件名进行排序 - aValue = (a.file_name_re || '').toLowerCase(); - bValue = (b.file_name_re || '').toLowerCase(); + // 对于重命名列,优先使用episode_number进行数值排序(如果存在) + if (a.episode_number !== undefined && b.episode_number !== undefined) { + aValue = a.episode_number; + bValue = b.episode_number; + } else { + // 否则使用重命名后的文件名进行字符串排序 + aValue = (a.file_name_re || '').toLowerCase(); + bValue = (b.file_name_re || '').toLowerCase(); + } } else if (field === 'size') { // 对于文件大小,使用数字进行排序 if (a.dir && b.dir) { @@ -3223,9 +3310,15 @@ aValue = a.file_name.toLowerCase(); bValue = b.file_name.toLowerCase(); } else if (field === 'file_name_re') { - // 对于重命名列,使用重命名后的文件名进行排序 - aValue = (a.file_name_re || '').toLowerCase(); - bValue = (b.file_name_re || '').toLowerCase(); + // 对于重命名列,优先使用episode_number进行数值排序(如果存在) + if (a.episode_number !== undefined && b.episode_number !== undefined) { + aValue = a.episode_number; + bValue = b.episode_number; + } else { + // 否则使用重命名后的文件名进行字符串排序 + aValue = (a.file_name_re || '').toLowerCase(); + bValue = (b.file_name_re || '').toLowerCase(); + } } else if (field === 'size') { // 对于文件大小,使用数字进行排序 if (a.dir && b.dir) { @@ -3684,6 +3777,23 @@ alert("刷新 AList 目录失败: " + (error.response?.data?.message || error.message || "未知错误")); }); }, + filterByTaskName(taskName, event) { + // 防止事件冒泡,避免触发行选择 + if (event) { + event.stopPropagation(); + } + + // 如果当前已经筛选了该任务,则取消筛选 + if (this.historyTaskSelected === taskName) { + this.historyTaskSelected = ""; + } else { + // 设置任务筛选值 + this.historyTaskSelected = taskName; + } + + // 重新加载记录 + this.loadHistoryRecords(); + }, } }); diff --git a/quark_auto_save.py b/quark_auto_save.py index befce4d..842ec15 100644 --- a/quark_auto_save.py +++ b/quark_auto_save.py @@ -141,12 +141,30 @@ def sort_file_by_name(file): if match_chinese: episode_value = int(match_chinese.group(1)) + # 2.1.1 "第[中文数字]期/集/话" 格式 + if episode_value == float('inf'): + match_chinese_num = re.search(r'第([一二三四五六七八九十百千万零两]+)[期集话]', filename) + if match_chinese_num: + chinese_num = match_chinese_num.group(1) + arabic_num = chinese_to_arabic(chinese_num) + if arabic_num is not None: + episode_value = arabic_num + # 2.2 "X集/期/话" 格式 if episode_value == float('inf'): match_chinese_simple = re.search(r'(\d+)[期集话]', filename) if match_chinese_simple: episode_value = int(match_chinese_simple.group(1)) + # 2.2.1 "[中文数字]集/期/话" 格式 + if episode_value == float('inf'): + match_chinese_simple_num = re.search(r'([一二三四五六七八九十百千万零两]+)[期集话]', filename) + if match_chinese_simple_num: + chinese_num = match_chinese_simple_num.group(1) + arabic_num = chinese_to_arabic(chinese_num) + if arabic_num is not None: + episode_value = arabic_num + # 2.3 S01E01格式 if episode_value == float('inf'): match_s_e = re.search(r'[Ss](\d+)[Ee](\d+)', filename) @@ -299,6 +317,16 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None): r'_?(\d+)_?' ] + # 添加中文数字匹配模式 + chinese_patterns = [ + r'第([一二三四五六七八九十百千万零两]+)集', + r'第([一二三四五六七八九十百千万零两]+)期', + r'第([一二三四五六七八九十百千万零两]+)话', + r'([一二三四五六七八九十百千万零两]+)集', + r'([一二三四五六七八九十百千万零两]+)期', + r'([一二三四五六七八九十百千万零两]+)话' + ] + patterns = None # 检查传入的episode_patterns参数 @@ -328,7 +356,19 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None): return episode_num except: continue - + + # 尝试匹配中文数字模式 + for pattern_regex in chinese_patterns: + try: + match = re.search(pattern_regex, filename_without_dates) + if match: + chinese_num = match.group(1) + arabic_num = chinese_to_arabic(chinese_num) + if arabic_num is not None: + return arabic_num + except: + continue + # 如果从不含日期的文件名中没有找到剧集号,尝试从原始文件名中提取 # 这是为了兼容某些特殊情况,但要检查提取的数字不是日期 file_name_without_ext = os.path.splitext(filename)[0] @@ -395,6 +435,71 @@ def is_date_format(number_str): # 其他格式不视为日期格式 return False +def chinese_to_arabic(chinese): + """ + 将中文数字转换为阿拉伯数字 + 支持格式:一、二、三、四、五、六、七、八、九、十、百、千、万 + 以及:零、两(特殊处理为2) + + Args: + chinese: 中文数字字符串 + + Returns: + int: 转换后的阿拉伯数字,如果无法转换则返回None + """ + if not chinese: + return None + + # 数字映射 + digit_map = { + '零': 0, '一': 1, '二': 2, '三': 3, '四': 4, + '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, + '两': 2 + } + + # 单位映射 + unit_map = { + '十': 10, + '百': 100, + '千': 1000, + '万': 10000 + } + + # 如果是单个字符,直接返回对应数字 + if len(chinese) == 1: + if chinese == '十': + return 10 + return digit_map.get(chinese) + + result = 0 + section = 0 + number = 0 + + # 从左向右处理 + for i in range(len(chinese)): + char = chinese[i] + + if char in digit_map: + number = digit_map[char] + elif char in unit_map: + unit = unit_map[char] + # 如果前面没有数字,默认为1,例如"十"表示1*10=10 + section += (number or 1) * unit + number = 0 + + # 如果是万级单位,累加到结果并重置section + if unit == 10000: + result += section + section = 0 + else: + # 非法字符 + return None + + # 加上最后的数字和小节 + result += section + number + + return result + # 兼容青龙 try: from treelib import Tree @@ -3133,7 +3238,7 @@ def format_bytes(size_bytes: int) -> str: while size_bytes >= 1024 and i < len(units) - 1: size_bytes /= 1024 i += 1 - return f"{size_bytes:.2f}{units[i]}" + return f"{size_bytes:.2f} {units[i]}" def do_sign(account): @@ -4258,6 +4363,11 @@ def do_save(account, tasklist=[]): number_part = filename[len(prefix):].split(suffix)[0] if suffix else filename[len(prefix):] if number_part.isdigit(): return int(number_part) + # 尝试转换中文数字 + else: + arabic_num = chinese_to_arabic(number_part) + if arabic_num is not None: + return arabic_num # 如果所有方法都失败,返回float('inf') return float('inf')