diff --git a/README.md b/README.md index 29d76d5..5abc64a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ - **WebUI**:对整个 WebUI 进行了重塑,增加了更多实用功能,如文件选择和预览界面的排序功能、资源搜索的过滤功能、TMDB 和豆瓣搜索功能、页面视图切换功能、账号设置功能等等。 - **查重逻辑**:支持优先通过历史转存记录查重,对于有转存记录的文件,即使删除网盘文件,也不会重复转存。 - **Aria2**:支持成功添加 Aria2 下载任务后自动删除夸克网盘内对应的文件,清理网盘空间。 +- **文件整理**:支持浏览和管理多个夸克账号的网盘文件,支持批量重命名(支持应用完整的命名、过滤规则和撤销重命名等操作)、删除文件等操作。 本项目修改后的版本为个人需求定制版,目的是满足我自己的使用需求,某些(我不用的)功能可能会因为修改而出现 BUG,不一定会被修复。若你要使用本项目,请知晓本人不是程序员,我无法保证本项目的稳定性,如果你在使用过程中发现了 BUG,可以在 Issues 中提交,但不保证每个 BUG 都能被修复,请谨慎使用,风险自担。 @@ -39,6 +40,7 @@ - [x] 转存后文件名整理(正则命名、**顺序命名**、**剧集命名**) - [x] 可选忽略文件后缀 - [x] **数据库记录所有转存历史(支持查看、查询和删除记录)** + - [x] **文件整理(支持浏览和管理多个夸克账号的网盘文件)** - 任务管理 - [x] 支持多组任务 @@ -54,7 +56,7 @@ - 其它 - [x] 每日签到领空间 [?](https://github.com/x1ao4/quark-auto-save-x/wiki/使用技巧集锦#每日签到领空间) - [x] 支持多个通知推送渠道 [?](https://github.com/x1ao4/quark-auto-save-x/wiki/通知推送服务配置) - - [x] 支持多账号(多账号签到,仅首账号转存) + - [x] 支持多账号(多账号签到、**文件管理**,仅首账号转存) - [x] 支持网盘文件下载、strm 文件生成等功能 [?](https://github.com/x1ao4/quark-auto-save-x/wiki/插件配置) ## 部署 diff --git a/app/run.py b/app/run.py index f4b16bd..88b6aa9 100644 --- a/app/run.py +++ b/app/run.py @@ -28,6 +28,8 @@ import re import random import time import treelib +from functools import lru_cache +from threading import Lock parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, parent_dir) @@ -96,6 +98,45 @@ PORT = int(os.environ.get("PORT", "5005")) config_data = {} task_plugins_config_default = {} +# 文件列表缓存 +file_list_cache = {} +cache_lock = Lock() + +# 默认性能参数(如果配置中没有设置) +DEFAULT_PERFORMANCE_CONFIG = { + "api_page_size": 200, + "cache_expire_time": 30 +} + +def get_performance_config(): + """获取性能配置参数""" + try: + if config_data and "file_performance" in config_data: + perf_config = config_data["file_performance"] + # 确保所有值都是整数类型 + result = {} + for key, default_value in DEFAULT_PERFORMANCE_CONFIG.items(): + try: + result[key] = int(perf_config.get(key, default_value)) + except (ValueError, TypeError): + result[key] = default_value + return result + except Exception as e: + print(f"获取性能配置失败: {e}") + return DEFAULT_PERFORMANCE_CONFIG + +def cleanup_expired_cache(): + """清理所有过期缓存""" + current_time = time.time() + perf_config = get_performance_config() + cache_expire_time = perf_config.get("cache_expire_time", 30) + + with cache_lock: + expired_keys = [k for k, (_, t) in file_list_cache.items() if current_time - t > cache_expire_time] + for k in expired_keys: + del file_list_cache[k] + return len(expired_keys) + app = Flask(__name__) app.config["APP_VERSION"] = get_app_ver() app.secret_key = "ca943f6db6dd34823d36ab08d8d6f65d" @@ -200,6 +241,21 @@ def get_data(): if not is_login(): return jsonify({"success": False, "message": "未登录"}) data = Config.read_json(CONFIG_PATH) + + # 处理插件配置中的多账号支持字段,将数组格式转换为逗号分隔的字符串用于显示 + if "plugins" in data: + # 处理Plex的quark_root_path + if "plex" in data["plugins"] and "quark_root_path" in data["plugins"]["plex"]: + data["plugins"]["plex"]["quark_root_path"] = format_array_config_for_display( + data["plugins"]["plex"]["quark_root_path"] + ) + + # 处理AList的storage_id + if "alist" in data["plugins"] and "storage_id" in data["plugins"]["alist"]: + data["plugins"]["alist"]["storage_id"] = format_array_config_for_display( + data["plugins"]["alist"]["storage_id"] + ) + # 发送webui信息,但不发送密码原文 data["webui"] = { "username": config_data["webui"]["username"], @@ -266,6 +322,21 @@ def sync_task_plugins_config(): current_config[key] = default_value +def parse_comma_separated_config(value): + """解析逗号分隔的配置字符串为数组""" + if isinstance(value, str) and value.strip(): + # 分割字符串,去除空白字符 + items = [item.strip() for item in value.split(',') if item.strip()] + # 如果只有一个项目,返回字符串(向后兼容) + return items[0] if len(items) == 1 else items + return value + +def format_array_config_for_display(value): + """将数组配置格式化为逗号分隔的字符串用于显示""" + if isinstance(value, list): + return ', '.join(value) + return value + # 更新数据 @app.route("/update", methods=["POST"]) def update(): @@ -279,6 +350,19 @@ def update(): # 更新webui凭据 config_data["webui"]["username"] = value.get("username", config_data["webui"]["username"]) config_data["webui"]["password"] = value.get("password", config_data["webui"]["password"]) + elif key == "plugins": + # 处理插件配置中的多账号支持字段 + if "plex" in value and "quark_root_path" in value["plex"]: + value["plex"]["quark_root_path"] = parse_comma_separated_config( + value["plex"]["quark_root_path"] + ) + + if "alist" in value and "storage_id" in value["alist"]: + value["alist"]["storage_id"] = parse_comma_separated_config( + value["alist"]["storage_id"] + ) + + config_data.update({key: value}) else: config_data.update({key: value}) @@ -400,6 +484,142 @@ def refresh_alist_directory(): return jsonify({"success": True, "message": "成功刷新 AList 目录"}) +# 文件整理页面刷新Plex媒体库 +@app.route("/refresh_filemanager_plex_library", methods=["POST"]) +def refresh_filemanager_plex_library(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + folder_path = request.json.get("folder_path") + account_index = request.json.get("account_index", 0) + + if not folder_path: + return jsonify({"success": False, "message": "缺少文件夹路径"}) + + # 检查Plex插件配置 + if not config_data.get("plugins", {}).get("plex", {}).get("url"): + return jsonify({"success": False, "message": "Plex 插件未配置"}) + + # 导入Plex插件 + from plugins.plex import Plex + + # 初始化Plex插件 + plex = Plex(**config_data["plugins"]["plex"]) + if not plex.is_active: + return jsonify({"success": False, "message": "Plex 插件未正确配置"}) + + # 获取夸克账号信息 + try: + account = Quark(config_data["cookie"][account_index], account_index) + + # 将文件夹路径转换为实际的保存路径 + # folder_path是相对于夸克网盘根目录的路径 + # quark_root_path是夸克网盘在本地文件系统中的挂载点 + # 根据账号索引获取对应的夸克根路径 + quark_root_path = plex.get_quark_root_path(account_index) + if not quark_root_path: + return jsonify({"success": False, "message": f"Plex 插件未配置账号 {account_index} 的夸克根路径"}) + + if folder_path == "" or folder_path == "/": + # 空字符串或根目录表示夸克网盘根目录 + full_path = quark_root_path + else: + # 确保路径格式正确 + if not folder_path.startswith("/"): + folder_path = "/" + folder_path + + # 拼接完整路径:夸克根路径 + 相对路径 + import os + full_path = os.path.normpath(os.path.join(quark_root_path, folder_path.lstrip("/"))).replace("\\", "/") + + # 确保库信息已加载 + if plex._libraries is None: + plex._libraries = plex._get_libraries() + + # 执行刷新 + success = plex.refresh(full_path) + + if success: + return jsonify({"success": True, "message": "成功刷新 Plex 媒体库"}) + else: + return jsonify({"success": False, "message": "刷新 Plex 媒体库失败,请检查路径配置"}) + + except Exception as e: + return jsonify({"success": False, "message": f"刷新 Plex 媒体库失败: {str(e)}"}) + + +# 文件整理页面刷新AList目录 +@app.route("/refresh_filemanager_alist_directory", methods=["POST"]) +def refresh_filemanager_alist_directory(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + folder_path = request.json.get("folder_path") + account_index = request.json.get("account_index", 0) + + if not folder_path: + return jsonify({"success": False, "message": "缺少文件夹路径"}) + + # 检查AList插件配置 + if not config_data.get("plugins", {}).get("alist", {}).get("url"): + return jsonify({"success": False, "message": "AList 插件未配置"}) + + # 导入AList插件 + from plugins.alist import Alist + + # 初始化AList插件 + alist = Alist(**config_data["plugins"]["alist"]) + if not alist.is_active: + return jsonify({"success": False, "message": "AList 插件未正确配置"}) + + # 获取夸克账号信息 + try: + account = Quark(config_data["cookie"][account_index], account_index) + + # 将文件夹路径转换为实际的保存路径 + # folder_path是相对于夸克网盘根目录的路径,如 "/" 或 "/测试/文件夹" + # 根据账号索引获取对应的存储配置 + storage_mount_path, quark_root_dir = alist.get_storage_config(account_index) + + if not storage_mount_path or not quark_root_dir: + return jsonify({"success": False, "message": f"AList 插件未配置账号 {account_index} 的存储信息"}) + + if folder_path == "/": + # 根目录,直接使用夸克根路径 + full_path = quark_root_dir + else: + # 子目录,拼接路径 + import os + # 移除folder_path开头的/,然后拼接 + relative_path = folder_path.lstrip("/") + if quark_root_dir == "/": + full_path = "/" + relative_path + else: + full_path = os.path.normpath(os.path.join(quark_root_dir, relative_path)).replace("\\", "/") + + # 检查路径是否在夸克根目录内 + if quark_root_dir == "/" or full_path.startswith(quark_root_dir): + # 使用账号对应的存储配置映射到AList路径 + # 构建AList路径 + if quark_root_dir == "/": + relative_path = full_path.lstrip("/") + else: + relative_path = full_path.replace(quark_root_dir, "", 1).lstrip("/") + + alist_path = os.path.normpath( + os.path.join(storage_mount_path, relative_path) + ).replace("\\", "/") + + # 执行刷新 + alist.refresh(alist_path) + return jsonify({"success": True, "message": "成功刷新 AList 目录"}) + else: + return jsonify({"success": False, "message": "路径不在AList配置的夸克根目录内"}) + + except Exception as e: + return jsonify({"success": False, "message": f"刷新 AList 目录失败: {str(e)}"}) + + @app.route("/task_suggestions") def get_task_suggestions(): if not is_login(): @@ -591,6 +811,22 @@ def get_share_detail(): episode_pattern = regex.get("episode_naming") episode_patterns = regex.get("episode_patterns", []) + # 获取默认的剧集模式 + default_episode_pattern = {"regex": '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?'} + + # 获取配置的剧集模式,确保每个模式都是字典格式 + episode_patterns = [] + raw_patterns = config_data.get("episode_patterns", [default_episode_pattern]) + for p in raw_patterns: + if isinstance(p, dict) and p.get("regex"): + episode_patterns.append(p) + elif isinstance(p, str): + episode_patterns.append({"regex": p}) + + # 如果没有有效的模式,使用默认模式 + if not episode_patterns: + episode_patterns = [default_episode_pattern] + # 添加中文数字匹配模式 chinese_patterns = [ {"regex": r'第([一二三四五六七八九十百千万零两]+)集'}, @@ -600,74 +836,7 @@ def get_share_detail(): {"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) - - # 构建剧集命名的正则表达式 (主要用于检测已命名文件) - if episode_pattern == "[]": - # 对于单独的[],使用特殊匹配 - regex_pattern = "^(\\d+)$" # 匹配纯数字文件名 - elif "[]" in episode_pattern: - # 特殊处理E[]、EP[]等常见格式,使用更宽松的匹配方式 - if episode_pattern == "E[]": - # 对于E[]格式,只检查文件名中是否包含形如E01的部分 - regex_pattern = "^E(\\d+)$" # 只匹配纯E+数字的文件名格式 - elif episode_pattern == "EP[]": - # 对于EP[]格式,只检查文件名中是否包含形如EP01的部分 - regex_pattern = "^EP(\\d+)$" # 只匹配纯EP+数字的文件名格式 - else: - # 对于其他带[]的格式,使用常规转义和替换 - regex_pattern = re.escape(episode_pattern).replace('\\[\\]', '(\\d+)') - else: - # 如果输入模式不包含[],则使用简单匹配模式,避免正则表达式错误 - regex_pattern = "^" + re.escape(episode_pattern) + "(\\d+)$" - - # 实现高级排序算法 - def extract_sorting_value(file): - if file["dir"]: # 跳过文件夹 - return (float('inf'), 0, 0, 0) # 返回元组以确保类型一致性 - - filename = file["file_name"] - - # 尝试获取剧集序号 - episode_num = extract_episode_number_local(filename) - if episode_num is not None: - # 返回元组以确保类型一致性 - return (0, episode_num, 0, 0) - - # 如果无法提取剧集号,则使用通用的排序函数 - return sort_file_by_name(file) - - # 过滤出非目录文件,并且排除已经符合命名规则的文件 - files_to_process = [] - for f in share_detail["list"]: - if f["dir"]: - continue # 跳过目录 - - # 检查文件是否已符合命名规则 - if episode_pattern == "[]": - # 对于单独的[],检查文件名是否为纯数字 - file_name_without_ext = os.path.splitext(f["file_name"])[0] - if file_name_without_ext.isdigit(): - # 增加判断:如果是日期格式的纯数字,不视为已命名 - if not is_date_format(file_name_without_ext): - continue # 跳过已符合命名规则的文件 - elif re.match(regex_pattern, f["file_name"]): - continue # 跳过已符合命名规则的文件 - - # 添加到待处理文件列表 - files_to_process.append(f) - - # 根据提取的排序值进行排序 - sorted_files = sorted(files_to_process, key=extract_sorting_value) + episode_patterns.extend(chinese_patterns) # 应用过滤词过滤 filterwords = regex.get("filterwords", "") @@ -675,32 +844,25 @@ def get_share_detail(): # 同时支持中英文逗号分隔 filterwords = filterwords.replace(",", ",") filterwords_list = [word.strip() for word in filterwords.split(',')] - for item in sorted_files: - # 被过滤的文件不会有file_name_re,与不匹配正则的文件显示一致 + for item in share_detail["list"]: + # 被过滤的文件显示一个 × if any(word in item['file_name'] for word in filterwords_list): item["filtered"] = True + item["file_name_re"] = "×" - # 为每个文件生成新文件名并存储剧集编号用于排序 - for file in sorted_files: - if not file.get("filtered"): - # 获取文件扩展名 - file_ext = os.path.splitext(file["file_name"])[1] - # 尝试提取剧集号 - episode_num = extract_episode_number_local(file["file_name"]) - if episode_num is not None: - # 生成预览文件名 - if episode_pattern == "[]": - # 对于单独的[],直接使用数字序号作为文件名 - 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 # 给一个很大的值,确保排在最后 + # 处理未被过滤的文件 + for file in share_detail["list"]: + if not file["dir"] and not file.get("filtered"): # 只处理未被过滤的非目录文件 + extension = os.path.splitext(file["file_name"])[1] + # 从文件名中提取集号 + episode_num = extract_episode_number(file["file_name"], episode_patterns=episode_patterns) + if episode_num is not None: + file["file_name_re"] = episode_pattern.replace("[]", f"{episode_num:02d}") + extension + else: + # 没有提取到集号,显示无法识别的提示 + file["file_name_re"] = "× 无法识别剧集编号" + return share_detail else: # 普通正则命名预览 @@ -741,7 +903,15 @@ def get_share_detail(): def get_savepath_detail(): if not is_login(): return jsonify({"success": False, "message": "未登录"}) - account = Quark(config_data["cookie"][0], 0) + + # 获取账号索引参数 + account_index = int(request.args.get("account_index", 0)) + + # 验证账号索引 + if account_index < 0 or account_index >= len(config_data["cookie"]): + return jsonify({"success": False, "message": "账号索引无效"}) + + account = Quark(config_data["cookie"][account_index], account_index) paths = [] if path := request.args.get("path"): if path == "/": @@ -776,7 +946,15 @@ def get_savepath_detail(): def delete_file(): if not is_login(): return jsonify({"success": False, "message": "未登录"}) - account = Quark(config_data["cookie"][0], 0) + + # 获取账号索引参数 + account_index = int(request.json.get("account_index", 0)) + + # 验证账号索引 + if account_index < 0 or account_index >= len(config_data["cookie"]): + return jsonify({"success": False, "message": "账号索引无效"}) + + account = Quark(config_data["cookie"][account_index], account_index) if fid := request.json.get("fid"): response = account.delete([fid]) @@ -863,6 +1041,15 @@ def add_task(): # 定时任务执行的函数 def run_python(args): logging.info(f">>> 定时运行任务") + + # 在定时任务开始前清理过期缓存 + try: + cleaned_count = cleanup_expired_cache() + if cleaned_count > 0: + logging.info(f">>> 清理了 {cleaned_count} 个过期缓存项") + except Exception as e: + logging.warning(f">>> 清理缓存时出错: {e}") + # 检查是否需要随机延迟执行 if delay := config_data.get("crontab_delay"): try: @@ -874,7 +1061,7 @@ def run_python(args): time.sleep(random_delay) except (ValueError, TypeError): logging.warning(f">>> 延迟执行设置无效: {delay}") - + os.system(f"{PYTHON_PATH} {args}") @@ -990,7 +1177,7 @@ def get_history_records(): # 如果请求所有任务名称,单独查询并返回 if get_all_task_names: cursor = db.conn.cursor() - cursor.execute("SELECT DISTINCT task_name FROM transfer_records ORDER BY task_name") + cursor.execute("SELECT DISTINCT task_name FROM transfer_records WHERE task_name NOT IN ('rename', 'undo_rename') ORDER BY task_name") all_task_names = [row[0] for row in cursor.fetchall()] # 如果同时请求分页数据,继续常规查询 @@ -1001,7 +1188,8 @@ def get_history_records(): sort_by=sort_by, order=order, task_name_filter=task_name_filter, - keyword_filter=keyword_filter + keyword_filter=keyword_filter, + exclude_task_names=["rename", "undo_rename"] ) # 添加所有任务名称到结果中 result["all_task_names"] = all_task_names @@ -1021,7 +1209,8 @@ def get_history_records(): sort_by=sort_by, order=order, task_name_filter=task_name_filter, - keyword_filter=keyword_filter + keyword_filter=keyword_filter, + exclude_task_names=["rename", "undo_rename"] ) # 处理记录格式化 @@ -1131,7 +1320,7 @@ def format_records(records): 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) @@ -1151,26 +1340,101 @@ def get_user_info(): "is_active": False, "has_mparam": has_mparam }) - + return jsonify({"success": True, "data": user_info_list}) +@app.route("/get_accounts_detail") +def get_accounts_detail(): + """获取所有账号的详细信息,包括昵称和空间使用情况""" + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + accounts_detail = [] + for idx, cookie in enumerate(config_data["cookie"]): + account = Quark(cookie, idx) + account_info = account.init() + + # 如果无法获取账号信息,检查是否有移动端参数 + if not account_info: + has_mparam = bool(account.mparam) + # 如果只有移动端参数,跳过此账号(不显示在文件整理页面的账号选择栏中) + if has_mparam: + continue + else: + # 如果既没有账号信息也没有移动端参数,显示为未登录 + account_detail = { + "index": idx, + "nickname": "", + "is_active": False, + "used_space": 0, + "total_space": 0, + "usage_rate": 0, + "display_text": f"账号{idx + 1}(未登录)" + } + accounts_detail.append(account_detail) + continue + + # 成功获取账号信息的情况 + account_detail = { + "index": idx, + "nickname": account_info["nickname"], + "is_active": account.is_active, + "used_space": 0, + "total_space": 0, + "usage_rate": 0, + "display_text": "" + } + + # 检查是否有移动端参数 + has_mparam = bool(account.mparam) + + if has_mparam: + # 同时有cookie和移动端参数,尝试获取空间信息 + try: + growth_info = account.get_growth_info() + if growth_info: + total_capacity = growth_info.get("total_capacity", 0) + account_detail["total_space"] = total_capacity + # 显示昵称和总容量 + total_str = format_bytes(total_capacity) + account_detail["display_text"] = f"{account_info['nickname']} · {total_str}" + else: + # 获取空间信息失败,只显示昵称 + account_detail["display_text"] = account_info["nickname"] + except Exception as e: + logging.error(f"获取账号 {idx} 空间信息失败: {str(e)}") + # 获取空间信息失败,只显示昵称 + account_detail["display_text"] = account_info["nickname"] + else: + # 只有cookie,没有移动端参数,只显示昵称 + account_detail["display_text"] = account_info["nickname"] + + accounts_detail.append(account_detail) + + return jsonify({"success": True, "data": accounts_detail}) + + # 重置文件夹(删除文件夹内所有文件和相关记录) @app.route("/reset_folder", methods=["POST"]) def reset_folder(): if not is_login(): return jsonify({"success": False, "message": "未登录"}) - + # 获取请求参数 save_path = request.json.get("save_path", "") - task_name = request.json.get("task_name", "") - + account_index = int(request.json.get("account_index", 0)) # 新增账号索引参数 + if not save_path: return jsonify({"success": False, "message": "保存路径不能为空"}) - + try: + # 验证账号索引 + if account_index < 0 or account_index >= len(config_data["cookie"]): + return jsonify({"success": False, "message": "账号索引无效"}) + # 初始化夸克网盘客户端 - account = Quark(config_data["cookie"][0], 0) + account = Quark(config_data["cookie"][account_index], account_index) # 1. 获取文件夹ID # 先检查是否已有缓存的文件夹ID @@ -1233,7 +1497,443 @@ def reset_folder(): return jsonify({"success": False, "message": f"重置文件夹时出错: {str(e)}"}) +# 获取文件列表 +@app.route("/file_list") +def get_file_list(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + # 获取请求参数 + folder_id = request.args.get("folder_id", "root") + sort_by = request.args.get("sort_by", "file_name") + order = request.args.get("order", "asc") + page = int(request.args.get("page", 1)) + page_size = int(request.args.get("page_size", 15)) + account_index = int(request.args.get("account_index", 0)) + force_refresh = request.args.get("force_refresh", "false").lower() == "true" # 新增账号索引参数 + + try: + # 验证账号索引 + if account_index < 0 or account_index >= len(config_data["cookie"]): + return jsonify({"success": False, "message": "账号索引无效"}) + + # 初始化夸克网盘客户端 + account = Quark(config_data["cookie"][account_index], account_index) + + # 获取文件列表 + if folder_id == "root": + folder_id = "0" # 根目录的ID为0 + paths = [] # 根目录没有路径 + else: + # 获取当前文件夹的路径 + paths = account.get_paths(folder_id) + + # 获取性能配置 + perf_config = get_performance_config() + api_page_size = perf_config.get("api_page_size", 200) + cache_expire_time = perf_config.get("cache_expire_time", 30) + + # 缓存键 + cache_key = f"{account_index}_{folder_id}_{sort_by}_{order}" + current_time = time.time() + + # 检查缓存(除非强制刷新) + if not force_refresh: + with cache_lock: + if cache_key in file_list_cache: + cache_data, cache_time = file_list_cache[cache_key] + if current_time - cache_time < cache_expire_time: + # 使用缓存数据 + cached_files = cache_data + total = len(cached_files) + start_idx = (page - 1) * page_size + end_idx = min(start_idx + page_size, total) + paginated_files = cached_files[start_idx:end_idx] + + return jsonify({ + "success": True, + "data": { + "list": paginated_files, + "total": total, + "paths": paths + } + }) + else: + # 强制刷新时,清除当前目录的缓存 + with cache_lock: + if cache_key in file_list_cache: + del file_list_cache[cache_key] + + # 无论分页还是全部模式,都必须获取所有文件才能进行正确的全局排序 + files = account.ls_dir(folder_id, page_size=api_page_size) + + if isinstance(files, dict) and files.get("error"): + # 检查是否是目录不存在的错误 + error_msg = files.get('error', '未知错误') + if "不存在" in error_msg or "无效" in error_msg or "找不到" in error_msg: + return jsonify({"success": False, "message": f"目录不存在或无权限访问: {error_msg}"}) + else: + return jsonify({"success": False, "message": f"获取文件列表失败: {error_msg}"}) + + # 计算总数 + total = len(files) + + # 优化排序:使用更高效的排序方法 + def get_sort_key(file_item): + if sort_by == "file_name": + return file_item["file_name"].lower() + elif sort_by == "file_size": + return file_item["size"] if not file_item["dir"] else 0 + else: # updated_at + return file_item["updated_at"] + + # 分离文件夹和文件以优化排序 + if sort_by == "updated_at": + # 修改日期排序时严格按照日期排序,不区分文件夹和文件 + files.sort(key=get_sort_key, reverse=(order == "desc")) + sorted_files = files + else: + # 其他排序时目录始终在前面,分别排序以提高效率 + directories = [f for f in files if f["dir"]] + normal_files = [f for f in files if not f["dir"]] + + directories.sort(key=get_sort_key, reverse=(order == "desc")) + normal_files.sort(key=get_sort_key, reverse=(order == "desc")) + + sorted_files = directories + normal_files + + # 更新缓存 + with cache_lock: + file_list_cache[cache_key] = (sorted_files, current_time) + + # 分页 + start_idx = (page - 1) * page_size + end_idx = min(start_idx + page_size, total) + paginated_files = sorted_files[start_idx:end_idx] + + return jsonify({ + "success": True, + "data": { + "list": paginated_files, + "total": total, + "paths": paths + } + }) + + except Exception as e: + logging.error(f">>> 获取文件列表时出错: {str(e)}") + return jsonify({"success": False, "message": f"获取文件列表时出错: {str(e)}"}) + + +# 预览重命名 +@app.route("/preview_rename") +def preview_rename(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + # 获取请求参数 + folder_id = request.args.get("folder_id", "root") + pattern = request.args.get("pattern", "") + replace = request.args.get("replace", "") + naming_mode = request.args.get("naming_mode", "regex") # regex, sequence, episode + include_folders = request.args.get("include_folders", "false") == "true" + filterwords = request.args.get("filterwords", "") + account_index = int(request.args.get("account_index", 0)) # 新增账号索引参数 + + if not pattern: + pattern = ".*" + + try: + # 验证账号索引 + if account_index < 0 or account_index >= len(config_data["cookie"]): + return jsonify({"success": False, "message": "账号索引无效"}) + + # 初始化夸克网盘客户端 + account = Quark(config_data["cookie"][account_index], account_index) + + # 获取文件列表 + if folder_id == "root": + folder_id = "0" # 根目录的ID为0 + + files = account.ls_dir(folder_id) + if isinstance(files, dict) and files.get("error"): + return jsonify({"success": False, "message": f"获取文件列表失败: {files.get('error', '未知错误')}"}) + + # 过滤要排除的文件 + # 替换中文逗号为英文逗号 + filterwords = filterwords.replace(",", ",") + filter_list = [keyword.strip() for keyword in filterwords.split(",") if keyword.strip()] + filtered_files = [] + for file in files: + # 如果不包含文件夹且当前项是文件夹,跳过 + if not include_folders and file["dir"]: + continue + + # 检查是否包含过滤关键词 + should_filter = False + for keyword in filter_list: + if keyword and keyword in file["file_name"]: + should_filter = True + break + + if not should_filter: + filtered_files.append(file) + + # 按不同命名模式处理 + preview_results = [] + + if naming_mode == "sequence": + # 顺序命名模式 + # 排序文件(按文件名或修改时间) + filtered_files.sort(key=lambda x: sort_file_by_name(x["file_name"])) + + sequence = 1 + for file in filtered_files: + extension = os.path.splitext(file["file_name"])[1] if not file["dir"] else "" + new_name = pattern.replace("{}", f"{sequence:02d}") + extension + preview_results.append({ + "original_name": file["file_name"], + "new_name": new_name, + "file_id": file["fid"] + }) + sequence += 1 + + elif naming_mode == "episode": + # 剧集命名模式 + # 获取默认的剧集模式 + default_episode_pattern = {"regex": '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?'} + + # 获取配置的剧集模式,确保每个模式都是字典格式 + episode_patterns = [] + raw_patterns = config_data.get("episode_patterns", [default_episode_pattern]) + for p in raw_patterns: + if isinstance(p, dict) and p.get("regex"): + episode_patterns.append(p) + elif isinstance(p, str): + episode_patterns.append({"regex": p}) + + # 如果没有有效的模式,使用默认模式 + if not episode_patterns: + episode_patterns = [default_episode_pattern] + + # 添加中文数字匹配模式 + chinese_patterns = [ + {"regex": r'第([一二三四五六七八九十百千万零两]+)集'}, + {"regex": r'第([一二三四五六七八九十百千万零两]+)期'}, + {"regex": r'第([一二三四五六七八九十百千万零两]+)话'}, + {"regex": r'([一二三四五六七八九十百千万零两]+)集'}, + {"regex": r'([一二三四五六七八九十百千万零两]+)期'}, + {"regex": r'([一二三四五六七八九十百千万零两]+)话'} + ] + episode_patterns.extend(chinese_patterns) + + # 处理每个文件 + for file in filtered_files: + extension = os.path.splitext(file["file_name"])[1] if not file["dir"] else "" + # 从文件名中提取集号 + episode_num = extract_episode_number(file["file_name"], episode_patterns=episode_patterns) + + if episode_num is not None: + new_name = pattern.replace("[]", f"{episode_num:02d}") + extension + preview_results.append({ + "original_name": file["file_name"], + "new_name": new_name, + "file_id": file["fid"] + }) + else: + # 没有提取到集号,显示无法识别的提示 + preview_results.append({ + "original_name": file["file_name"], + "new_name": "× 无法识别剧集编号", + "file_id": file["fid"] + }) + + else: + # 正则命名模式 + for file in filtered_files: + try: + # 应用正则表达式 + if replace: + new_name = re.sub(pattern, replace, file["file_name"]) + else: + # 如果没有提供替换表达式,则检查是否匹配 + if re.search(pattern, file["file_name"]): + new_name = file["file_name"] # 匹配但不替换 + else: + new_name = "" # 表示不匹配 + + preview_results.append({ + "original_name": file["file_name"], + "new_name": new_name if new_name != file["file_name"] else "", # 如果没有改变,返回空表示不重命名 + "file_id": file["fid"] + }) + except Exception as e: + # 正则表达式错误 + preview_results.append({ + "original_name": file["file_name"], + "new_name": "", # 表示无法重命名 + "file_id": file["fid"], + "error": str(e) + }) + + return jsonify({ + "success": True, + "data": preview_results + }) + + except Exception as e: + logging.error(f">>> 预览重命名时出错: {str(e)}") + return jsonify({"success": False, "message": f"预览重命名时出错: {str(e)}"}) + + +# 批量重命名 +@app.route("/batch_rename", methods=["POST"]) +def batch_rename(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + # 获取请求参数 + data = request.json + files = data.get("files", []) + account_index = int(data.get("account_index", 0)) # 新增账号索引参数 + + if not files: + return jsonify({"success": False, "message": "没有文件需要重命名"}) + + try: + # 验证账号索引 + if account_index < 0 or account_index >= len(config_data["cookie"]): + return jsonify({"success": False, "message": "账号索引无效"}) + + # 初始化夸克网盘客户端 + account = Quark(config_data["cookie"][account_index], account_index) + + # 批量重命名 + success_count = 0 + failed_files = [] + save_path = data.get("save_path", "") + batch_time = int(time.time() * 1000) # 批次时间戳,确保同一批transfer_time一致 + for file_item in files: + file_id = file_item.get("file_id") + new_name = file_item.get("new_name") + original_name = file_item.get("old_name") or "" + if file_id and new_name: + try: + result = account.rename(file_id, new_name) + if result.get("code") == 0: + success_count += 1 + # 记录重命名 + record_db.add_record( + task_name="rename", + original_name=original_name, + renamed_to=new_name, + file_size=0, + modify_date=int(time.time()), + file_id=file_id, + file_type="file", + save_path=save_path, + transfer_time=batch_time + ) + else: + failed_files.append({ + "file_id": file_id, + "new_name": new_name, + "error": result.get("message", "未知错误") + }) + except Exception as e: + failed_files.append({ + "file_id": file_id, + "new_name": new_name, + "error": str(e) + }) + + return jsonify({ + "success": True, + "message": f"成功重命名 {success_count} 个文件,失败 {len(failed_files)} 个", + "success_count": success_count, + "failed_files": failed_files + }) + + except Exception as e: + logging.error(f">>> 批量重命名时出错: {str(e)}") + return jsonify({"success": False, "message": f"批量重命名时出错: {str(e)}"}) + + +# 撤销重命名接口 +@app.route("/undo_rename", methods=["POST"]) +def undo_rename(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + data = request.json + save_path = data.get("save_path", "") + account_index = int(data.get("account_index", 0)) # 新增账号索引参数 + + if not save_path: + return jsonify({"success": False, "message": "缺少目录参数"}) + + try: + # 验证账号索引 + if account_index < 0 or account_index >= len(config_data["cookie"]): + return jsonify({"success": False, "message": "账号索引无效"}) + + account = Quark(config_data["cookie"][account_index], account_index) + # 查询该目录下最近一次重命名(按transfer_time分组,task_name=rename) + records = record_db.get_records_by_save_path(save_path) + rename_records = [r for r in records if r["task_name"] == "rename"] + if not rename_records: + return jsonify({"success": False, "message": "没有可撤销的重命名记录"}) + # 找到最近一批(transfer_time最大值) + latest_time = max(r["transfer_time"] for r in rename_records) + latest_batch = [r for r in rename_records if r["transfer_time"] == latest_time] + success_count = 0 + failed_files = [] + deleted_ids = [] + for rec in latest_batch: + file_id = rec["file_id"] + old_name = rec["original_name"] + try: + result = account.rename(file_id, old_name) + if result.get("code") == 0: + success_count += 1 + deleted_ids.append(rec["id"]) + else: + failed_files.append({ + "file_id": file_id, + "old_name": old_name, + "error": result.get("message", "未知错误") + }) + except Exception as e: + failed_files.append({ + "file_id": file_id, + "old_name": old_name, + "error": str(e) + }) + # 撤销成功的,直接删除对应rename记录 + for rid in deleted_ids: + record_db.delete_record(rid) + return jsonify({ + "success": True, + "message": f"成功撤销 {success_count} 个文件重命名,失败 {len(failed_files)} 个", + "success_count": success_count, + "failed_files": failed_files + }) + except Exception as e: + logging.error(f">>> 撤销重命名时出错: {str(e)}") + return jsonify({"success": False, "message": f"撤销重命名时出错: {str(e)}"}) + + +@app.route("/api/has_rename_record") +def has_rename_record(): + save_path = request.args.get("save_path", "") + db = RecordDB() + records = db.get_records_by_save_path(save_path) + has_rename = any(r["task_name"] == "rename" for r in records) + return jsonify({"has_rename": has_rename}) + + if __name__ == "__main__": init() reload_tasks() + # 初始化全局db对象,确保所有接口可用 + record_db = RecordDB() app.run(debug=DEBUG, host="0.0.0.0", port=PORT) diff --git a/app/sdk/db.py b/app/sdk/db.py index 316de4e..3321f83 100644 --- a/app/sdk/db.py +++ b/app/sdk/db.py @@ -15,7 +15,7 @@ class RecordDB: os.makedirs(os.path.dirname(self.db_path), exist_ok=True) # 创建数据库连接 - self.conn = sqlite3.connect(self.db_path) + self.conn = sqlite3.connect(self.db_path, check_same_thread=False) cursor = self.conn.cursor() # 创建表,如果不存在 @@ -49,13 +49,14 @@ class RecordDB: self.conn.close() def add_record(self, task_name, original_name, renamed_to, file_size, modify_date, - duration="", resolution="", file_id="", file_type="", save_path=""): + duration="", resolution="", file_id="", file_type="", save_path="", transfer_time=None): """添加一条转存记录""" cursor = self.conn.cursor() + now_ms = int(time.time() * 1000) if transfer_time is None else transfer_time cursor.execute( "INSERT INTO transfer_records (transfer_time, task_name, original_name, renamed_to, file_size, " "duration, resolution, modify_date, file_id, file_type, save_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - (int(time.time()), task_name, original_name, renamed_to, file_size, + (now_ms, task_name, original_name, renamed_to, file_size, duration, resolution, modify_date, file_id, file_type, save_path) ) self.conn.commit() @@ -123,7 +124,7 @@ class RecordDB: return 0 def get_records(self, page=1, page_size=20, sort_by="transfer_time", order="desc", - task_name_filter="", keyword_filter=""): + task_name_filter="", keyword_filter="", exclude_task_names=None): """获取转存记录列表,支持分页、排序和筛选 Args: @@ -133,6 +134,7 @@ class RecordDB: order: 排序方向(asc/desc) task_name_filter: 任务名称筛选条件(精确匹配) keyword_filter: 关键字筛选条件(模糊匹配任务名) + exclude_task_names: 需要排除的任务名称列表 """ cursor = self.conn.cursor() offset = (page - 1) * page_size @@ -159,6 +161,10 @@ class RecordDB: params.append(f"%{keyword_filter}%") params.append(f"%{keyword_filter}%") + if exclude_task_names: + where_clauses.append("task_name NOT IN ({})".format(",".join(["?" for _ in exclude_task_names]))) + params.extend(exclude_task_names) + where_clause = " AND ".join(where_clauses) where_sql = f"WHERE {where_clause}" if where_clause else "" diff --git a/app/static/css/main.css b/app/static/css/main.css index 8311f99..c479fc1 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -94,15 +94,17 @@ body.login-page { z-index: 9999; width: auto; max-width: 80%; + pointer-events: none; } .toast-custom { - min-width: 134px; + min-width: 120px; background-color: var(--focus-border-color); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1); border: none; border-radius: 6px; margin: 0 auto; + width: fit-content; } .toast-body-custom { @@ -1131,8 +1133,8 @@ textarea.form-control { .expand-button { position: absolute; right: 5px; - top: 10.5px; /* 恢复为固定位置 */ - transform: none; /* 移除垂直居中的转换 */ + top: 11px; /* 固定位置,不再使用百分比定位 */ + transform: none; /* 移除垂直居中转换 */ cursor: pointer; opacity: 0; transition: opacity 0.2s; @@ -1152,12 +1154,11 @@ textarea.form-control { .expanded-text { white-space: normal; - padding: 0 0; /* 展开时的上下内边距 */ word-break: break-all; display: block; /* 确保展开的内容是块级元素 */ - line-height: 1.5; /* 设置合理的行高 */ - margin-top: 4px; /* 与上方内容保持一定间距 */ - margin-bottom: 4px; /* 添加底部边距,与表格底部分割线保持一致的距离 */ + line-height: 24px !important; /* 与表头保持一致的行高 */ + margin-top: 0; /* 移除顶部边距 */ + margin-bottom: 0; /* 移除底部边距 */ position: relative; /* 确保定位准确 */ padding-right: 25px; /* 为展开按钮预留空间 */ } @@ -1174,25 +1175,18 @@ textarea.form-control { overflow-x: auto; } -.table th { - white-space: nowrap; - background-color: #f8f9fa; - position: sticky; - top: 0; - z-index: 10; -} - /* 表头样式调整 */ -.table thead th { +.table thead th, +table.table thead th { vertical-align: middle; - border-bottom: 1px solid var(--border-color); /* 修改底部边框为1px */ - border-top: 1px solid var(--border-color); /* 添加上边框线 */ - padding-top: 8.5px; /* 增加上内边距 */ - padding-bottom: 8.5px; /* 增加下内边距 */ - padding-left: 9px !important; /* 表头左内边距,与按钮一致 */ - padding-right: 9px !important; /* 表头右内边距,与按钮一致 */ - background-color: var(--button-gray-background-color); /* 表头背景色 */ - color: var(--dark-text-color); /* 表头文字颜色 */ + border-bottom: 1px solid var(--border-color) !important; /* 修改底部边框为1px */ + border-top: 1px solid var(--border-color) !important; /* 添加上边框线 */ + padding: 8px 9px !important; /* 统一表头内边距 */ + background-color: var(--button-gray-background-color) !important; /* 表头背景色 */ + color: var(--dark-text-color) !important; /* 表头文字颜色 */ + height: 40px !important; /* 确保表头高度为40px */ + line-height: 24px !important; /* 设置行高以确保文字垂直居中 */ + box-sizing: border-box !important; /* 确保边框包含在总高度内 */ } /* 表头悬停样式 */ @@ -1202,18 +1196,17 @@ textarea.form-control { /* 表格单元格样式 */ .table td { - vertical-align: top; /* 改为顶部对齐,避免内容居中问题 */ - height: auto; /* 根据内容自动调整高度 */ + vertical-align: middle !important; /* 统一使用垂直居中对齐 */ + height: 40px !important; /* 与表头保持一致的高度 */ max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - padding-top: 8px; /* 单元格上内边距 */ - padding-bottom: 8px; /* 单元格下内边距 */ - padding-left: 9px !important; /* 单元格左内边距,与按钮一致 */ - padding-right: 9px !important; /* 单元格右内边距,与按钮一致 */ + padding: 8px 9px !important; /* 统一单元格内边距 */ border-bottom: 1px solid var(--border-color); /* 单元格分割线颜色 */ - transform: translateY(0.5px); /* 文本下移0.5px,不影响元素实际高度 */ + border-top: none !important; /* 确保没有上边框 */ + box-sizing: border-box !important; /* 确保边框包含在总高度内 */ + line-height: 24px !important; /* 与表头保持一致的行高 */ } /* 表格行悬停样式 */ @@ -1227,23 +1220,22 @@ textarea.form-control { word-break: break-word; } -/* 新增:设置表格单元格中的position-relative样式,以便正确定位内容 */ +/* 设置表格单元格中的position-relative样式,以便正确定位内容 */ .table td.position-relative { position: relative; /* 确保相对定位生效 */ - padding-top: 4px; /* 减少顶部内边距 */ - padding-bottom: 4px; /* 减少底部内边距 */ + padding: 8px 9px !important; /* 保持与其他单元格一致的内边距 */ + vertical-align: top !important; /* 确保内容顶部对齐 */ } -/* 新增:调整text-truncate在表格单元格中的布局 */ +/* 调整text-truncate在表格单元格中的布局 */ .table td .text-truncate { display: block; /* 使其成为块级元素 */ max-width: 100%; /* 确保不超出单元格宽度 */ padding-right: 25px; /* 为展开按钮留出空间 */ - padding-top: 4px; /* 与单元格顶部保持一定距离 */ - padding-bottom: 4px; /* 与单元格底部保持一定距离 */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + line-height: 24px !important; /* 与表头保持一致的行高 */ } /* --------------- 模态框样式 --------------- */ @@ -1606,7 +1598,7 @@ button.close:focus, top: 0; z-index: 5; vertical-align: middle; /* 添加:垂直居中对齐 */ - height: auto; /* 添加:自动高度,确保与内容一致 */ + height: 40px !important; /* 添加:自动高度,确保与内容一致 */ } /* 模态框表格列宽设置 - 基于内容类型 */ @@ -1689,15 +1681,6 @@ button.close:focus, cursor: pointer; } -/* 弹窗内文件夹和文件图标样式 */ -#fileSelectModal .bi-folder-fill { - color: #ffc107; - font-size: 0.9rem; - margin-right: 5px; - position: relative; - top: 1px; /* 负值向上移动,正值向下移动 */ -} - #fileSelectModal .bi-file-earmark { color: var(--dark-text-color); font-size: 0.9rem; @@ -2150,6 +2133,10 @@ div.jsoneditor-tree button.jsoneditor-button:focus { font-size: 1rem; } +.sidebar .nav-link .bi-archive { + font-size: 1rem; +} + .sidebar .nav-link .bi-power { font-size: 1.27rem; } @@ -3345,7 +3332,7 @@ div[id^="collapse_"][id*="plugin"] .input-group { #fileSelectModal .badge-info { background-color: var(--focus-border-color) !important; color: white !important; - font-size: 0.84rem !important; + font-size: 0.83rem !important; padding: 6px 3px !important; font-weight: normal !important; border-radius: 4px !important; @@ -3386,7 +3373,7 @@ div[id^="collapse_"][id*="plugin"] .input-group { /* 多行表达式间距 - 针对预览区域显示的多行表达式 */ #fileSelectModal .mb-3[v-if="fileSelect.previewRegex"] > div > span.badge-info + span.badge-info { - margin-top: 1px !important; /* 减少多行表达式之间的间距 */ + margin-top: 5px !important; /* 减少多行表达式之间的间距 */ display: inline-block; } @@ -3602,10 +3589,22 @@ input::-moz-list-button { padding-right: 16px !important; /* 强制设置右边距为16px */ box-sizing: border-box; } - + #fileSelectModal[data-modal-type="preview"] .table { width: 460px; } + + /* 针对文件整理页面命名预览模式 - 2列表格 */ + #fileSelectModal[data-modal-type="preview-filemanager"] .breadcrumb { + min-width: 460px; /* 2列表格总宽度: 230px + 230px */ + margin-right: 0; + padding-right: 16px !important; /* 强制设置右边距为16px */ + box-sizing: border-box; + } + + #fileSelectModal[data-modal-type="preview-filemanager"] .table { + width: 460px; + } /* 确保面包屑导航内容不被截断 */ #fileSelectModal .breadcrumb-item { @@ -3688,7 +3687,7 @@ input::-moz-list-button { #fileSelectModal .expand-button { position: absolute; right: 5px; - top: 7.5px; /* 固定高度,不再使用百分比定位 */ + top: 7.5px; /* 固定位置 */ transform: none; /* 移除垂直居中转换 */ cursor: pointer; opacity: 0; @@ -3717,6 +3716,7 @@ input::-moz-list-button { /* 确保模态框中的表格单元格可以正确显示展开按钮 */ #fileSelectModal .table td.position-relative { position: relative; + vertical-align: top !important; /* 确保内容顶部对齐 */ } /* 当表格行被点击时,防止展开按钮的点击事件冒泡 */ @@ -3728,6 +3728,8 @@ input::-moz-list-button { #fileSelectModal .table td [style*="white-space: normal"] { display: block; width: 100%; + margin-top: 0; /* 移除顶部边距 */ + margin-bottom: 7px; } /* 确保表格行内容保持顶部对齐 */ @@ -3799,24 +3801,8 @@ input.no-spinner { /* 文件大小值的文本位置调整 */ .file-size-cell .file-size-value { - transform: translateY(-1px); /* 文本下移 */ display: inline-block; /* 确保transform生效 */ -} - -/* 转存记录删除按钮样式 */ -.delete-record-btn { - color: #dc3545; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 4px; - transition: background-color 0.2s ease; - visibility: hidden; /* 默认隐藏 */ - position: relative; /* 添加相对定位 */ - top: -0.5px; /* 上移 */ + transform: translateY(0px); } /* 删除按钮图标大小 */ @@ -3845,6 +3831,7 @@ table.selectable-records { table.selectable-records tbody tr { cursor: pointer; + transition: none; } /* 修改表格行悬停样式,使用变量保持一致性 */ @@ -3857,12 +3844,16 @@ tr.selected-record .expand-button { z-index: 2; } -/* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */ -table.selectable-records .expand-button:hover, -table.selectable-records .delete-record-btn:hover { +/* 当鼠标悬停在展开按钮上时,不改变按钮的背景色 */ +table.selectable-records .expand-button:hover { background-color: transparent !important; } +/* 转存记录页面删除按钮悬停效果 */ +table.selectable-records .delete-record-btn:hover { + color: #b02a37 !important; +} + /* 选中行或鼠标悬停行的大小列样式 */ tr.selected-record .file-size-cell .file-size-value, .selectable-records tbody tr:hover .file-size-cell .file-size-value { @@ -3878,6 +3869,10 @@ tr.selected-record .file-size-cell .delete-record-btn, height: 100%; margin-left: 0; /* 确保没有左边距 */ padding-left: 0; /* 确保没有左内边距 */ + left: 9px; /* 与大小值保持相同的左边距 */ + position: absolute; /* 使用绝对定位 */ + top: 8px; /* 固定位置 */ + transform: none; /* 移除垂直居中转换 */ } /* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */ @@ -3898,24 +3893,38 @@ table.selectable-records .expand-button:hover { /* 特别调整红色"×"符号的位置 */ #fileSelectModal .table td.col-rename.text-danger div:not(.expand-button) { position: relative; - top: -1px; /* 将"×"标记上移1px */ + top: 2px !important; /* 将"×"标记上移1px */ } -/* 文件名列已经通过图标微调过,保持原样或细微调整 */ +#fileSelectModal[data-modal-type="preview"] .table td.col-rename > * { + position: relative; + top: 3px !important; +} + +/* 文件整理页面命名预览模式下的重命名列通用样式 */ +#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename > * { + position: relative; + top: 3px !important; /* 与任务配置页面保持一致 */ +} + +/* 模态框通用文件夹图标样式 */ #fileSelectModal .bi-folder-fill { color: #ffc107; - font-size: 0.9rem; - margin-right: 5px; + font-size: 0.95rem; + margin-right: 4px !important; position: relative; - top: 1px; /* 负值向上移动,正值向下移动 */ + top: 0.5px !important; /* 负值向上移动,正值向下移动 */ + left: -0.55px; /* 整体向左移动 */ } +/* 模态框通用文件图标样式 */ #fileSelectModal .bi-file-earmark { color: var(--dark-text-color); - font-size: 0.9rem; - margin-right: 5px; + font-size: 0.95rem; + margin-right: 4px !important; position: relative; - top: 0px; /* 负值向上移动,正值向下移动 */ + top: 0.5px !important; /* 负值向上移动,正值向下移动 */ + left: -0.4px; } /* 添加选中文件的样式 */ @@ -3962,14 +3971,14 @@ table.selectable-records .expand-button:hover { /* Plex图标样式 */ .plex-icon { - width: 10.4px; + width: 10.3px; height: auto; object-fit: contain; } /* AList图标样式 */ .alist-icon { - width: 17.8px; + width: 18px; height: auto; object-fit: contain; position: relative; @@ -4034,6 +4043,21 @@ table.selectable-records .expand-button:hover { margin-right: -4px !important; } +/* 文件整理性能设置样式 */ +.performance-setting-row > [class*='col-'] { + padding-left: 4px !important; + padding-right: 4px !important; +} +.performance-setting-row { + margin-left: -4px !important; + margin-right: -4px !important; +} + +/* 性能设置板块标题上移8px,统一与其他设置板块的间距 */ +.row.title[title*="文件整理页面的请求参数"] { + margin-top: 12px !important; /* 从默认的20px减少到12px,上移8px */ +} + /* 任务单元基础样式 */ .task { position: relative; @@ -4129,3 +4153,1149 @@ select.task-filter-select, .task-name-hover:hover { color: var(--focus-border-color) !important; } + +/* 文件整理页面样式 */ +.file-manager-card { + display: none; /* 隐藏卡片 */ +} + +.file-manager-card .card-body { + padding: 0; /* 移除内边距 */ +} + +.file-manager-input { + flex: 1; +} + +/* 文件整理页面面包屑导航样式 */ +.file-manager-breadcrumb { + margin-top: 20px; +} + +.file-manager-breadcrumb .breadcrumb-item { + color: var(--dark-text-color); + padding: 0; + position: relative; /* 添加相对定位 */ +} + +.file-manager-breadcrumb .breadcrumb-item a { + color: var(--dark-text-color); + text-decoration: none; +} + +.file-manager-breadcrumb .breadcrumb-item a:hover { + color: var(--focus-border-color); +} + +/* 隐藏Bootstrap默认的面包屑分隔符 */ +.file-manager-breadcrumb .breadcrumb-item + .breadcrumb-item::before { + color: var(--dark-text-color); + content: "/"; /* 使用斜杠作为分隔符 */ + position: relative; + display: inline-block; + vertical-align: middle; /* 垂直居中 */ + padding: 0 8px; /* 在斜杠两侧添加间距 */ + top: -1px; /* 将分隔符上移1px */ +} + +.file-actions { + white-space: nowrap; +} + +.selectable-files tr.selected-file { + background-color: rgba(var(--primary-rgb), 0.1); +} + +.batch-rename-btn { + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px !important; + color: var(--dark-text-color) !important; + border-color: var(--dark-text-color) !important; +} + +/* 为相邻的batch-rename-btn按钮添加左边距 */ +.batch-rename-btn + .batch-rename-btn { + margin-left: 8px; +} + +/* 文件整理页面按钮与前面元素的间距 */ +.file-manager-rule-bar .batch-rename-btn:first-of-type { + margin-left: 8px; +} + +/* 移动端文件整理页面按钮与前面元素的间距 */ +.file-manager-rule-bar-responsive .batch-rename-btn:first-of-type { + margin-left: 8px; +} + +.batch-rename-btn:hover { + background-color: var(--dark-text-color) !important; + border-color: var(--dark-text-color) !important; + color: white !important; +} + +/* 文件整理页面刷新当前目录缓存按钮图标大小 */ +.batch-rename-btn .bi-arrow-clockwise { + font-size: 1.17rem; +} + +/* 确保文件整理页面的Plex和AList按钮样式与任务列表一致 */ +.batch-rename-btn.btn-outline-plex, +.batch-rename-btn.btn-outline-alist { + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px !important; +} + +/* 文件整理页面Plex按钮样式 */ +.batch-rename-btn.btn-outline-plex { + border-color: #EBAF00 !important; + color: #EBAF00 !important; +} + +.batch-rename-btn.btn-outline-plex:hover { + background-color: #EBAF00 !important; + border-color: #EBAF00 !important; + color: #fff !important; +} + +.batch-rename-btn.btn-outline-plex:hover .plex-icon { + filter: brightness(0) invert(1); +} + +/* 文件整理页面AList按钮样式 */ +.batch-rename-btn.btn-outline-alist { + border-color: #70C6BE !important; + color: #70C6BE !important; +} + +.batch-rename-btn.btn-outline-alist:hover { + background-color: #70C6BE !important; + border-color: #70C6BE !important; + color: #fff !important; +} + +.batch-rename-btn.btn-outline-alist:hover .alist-icon { + filter: brightness(0) invert(1); +} + +/* 文件表格中的展开按钮 */ +.expand-button { + position: absolute; + right: 5px; + top: 11px; /* 固定位置,与其他表格保持一致 */ + transform: none; /* 移除垂直居中转换 */ + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; + background: #fff; + border-radius: 50%; + width: 18px; + height: 18px; + text-align: center; + line-height: 18px; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); + z-index: 2; +} + +.expand-button:hover { + opacity: 1; +} + +/* 批量重命名模态框样式 */ +#batchRenameModal .modal-dialog { + max-width: 80%; +} + +#batchRenameModal .table { + margin-bottom: 0; +} + +#batchRenameModal .badge { + font-size: 90%; + font-weight: 500; + word-break: break-all; +} + +#batchRenameModal .alert { + margin-bottom: 15px; +} + +#batchRenameModal .text-danger { + background-color: rgba(var(--danger-rgb), 0.05); +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .file-manager-rule-bar { + flex-direction: column; + } + + .file-manager-rule-bar .input-group { + margin-bottom: 10px; + } + + .batch-rename-btn { + margin-top: 10px; + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px !important; + } + + /* 移动端按钮间距重置 */ + .batch-rename-btn + .batch-rename-btn { + margin-left: 8px; + } + + /* 移动端第一个按钮与前面元素的间距 */ + .file-manager-rule-bar-responsive .batch-rename-btn:first-of-type { + margin-left: 8px; + } + + #batchRenameModal .modal-dialog { + max-width: 95%; + } +} + +/* 文件大小单元格样式 */ +.file-size-cell { + position: relative; +} + +.file-size-cell .file-size-value { + display: inline-block; +} + +.delete-record-btn .bi-trash3 { + font-size: 1rem; +} + +/* 当行被选中或鼠标悬停时显示删除按钮 */ +tr:hover .delete-record-btn, +tr.selected-file .delete-record-btn { + display: inline-block; +} + +/* 表格标题行不显示删除按钮 */ +table th .delete-record-btn { + display: none !important; +} + +/* 可选择的表格样式 */ +table.selectable-files { + border-collapse: separate; + border-spacing: 0; +} + +table.selectable-files tbody tr { + transition: none; +} + +table.selectable-files tbody tr:hover { + background-color: rgba(var(--primary-rgb), 0.05); +} + +/* 选中行时展开按钮的样式 */ +tr.selected-file .expand-button { + background-color: var(--button-gray-background-color); +} + +/* 展开按钮悬停样式 */ +table.selectable-files .expand-button:hover, +tr.selected-file .expand-button:hover { + background-color: #fff; + opacity: 1; +} + +/* 选中行时文件大小和删除按钮的样式 */ +tr.selected-file .file-size-cell .file-size-value, +tr.selected-file .file-size-cell .delete-record-btn { + position: relative; + z-index: 1; +} + +/* 文件整理页面表格样式 */ +.selectable-files { + border-collapse: collapse !important; +} + +.selectable-files td, +.selectable-files th { + border: none !important; + border-top: 1px solid var(--border-color) !important; /* 添加顶部边框线 */ + border-bottom: 1px solid var(--border-color) !important; /* 添加底部分割线 */ +} + +.selectable-files tbody tr { + cursor: pointer; + transition: none; +} + +.selectable-files tbody tr:hover { + background-color: var(--button-gray-background-color) !important; +} + +/* 删除按钮样式调整 */ +.selectable-files .delete-record-btn { + color: #dc3545; + cursor: pointer; + display: none; + align-items: center; + justify-content: flex-start; /* 居左对齐 */ + width: 24px; + height: 24px; + border-radius: 4px; + transition: background-color 0.2s ease; + position: absolute; + right: auto; /* 移除右对齐 */ + left: 9px; /* 与大小值保持相同的左边距 */ + top: 50%; + transform: translateY(-50%); +} + +.selectable-files tr:hover .delete-record-btn, +.selectable-files tr.selected-file .delete-record-btn { + display: inline-flex; +} + +.selectable-files tr:hover .file-size-value, +.selectable-files tr.selected-file .file-size-value { + visibility: hidden; +} + +/* 确保选中行的样式正确 */ +.selectable-files tr.selected-file { + background-color: var(--button-gray-background-color) !important; +} + +/* 使面包屑导航样式与表头一致 */ +.file-manager-breadcrumb .breadcrumb { + background-color: var(--button-gray-background-color) !important; + border-radius: 0px; + font-size: 0.95rem; + padding: 0 9px !important; + margin-bottom: 8px; + border-top: 1px solid var(--border-color) !important; + border-bottom: 1px solid var(--border-color) !important; + display: flex; + align-items: center; + height: 42px !important; /* 确保面包屑导航内容区域高度为42px(包含上下边框) */ + line-height: 23px !important; /* 设置行高以确保文字垂直居中 */ + box-sizing: border-box !important; /* 确保边框包含在总高度内 */ +} + +.file-manager-breadcrumb .breadcrumb-item { + color: var(--dark-text-color); + padding: 0; + position: relative; /* 添加相对定位 */ +} + +.file-manager-breadcrumb .breadcrumb-item a { + color: var(--dark-text-color); + text-decoration: none; +} + +.file-manager-breadcrumb .breadcrumb-item a:hover { + color: var(--focus-border-color); +} + +/* 让"全部文件"链接悬停时文字变为蓝色 */ +.file-manager-breadcrumb .breadcrumb-item.cursor-pointer:hover { + background-color: transparent; /* 移除背景色 */ + color: var(--focus-border-color); +} + +.file-manager-breadcrumb .breadcrumb-item.cursor-pointer:hover { + color: var(--focus-border-color); +} + +/* 文件整理页面样式 */ +.file-manager-input { + flex: 1; +} + +/* 文件整理页面面包屑导航样式 */ +.file-manager-breadcrumb { + margin-top: 20px; +} + +/* 文件整理规则栏样式 */ +.file-manager-rule-bar { + width: 100%; +} + +/* 文件整理账号选择栏样式 */ +.file-manager-account-selector { + margin-bottom: 8px !important; +} + +/* 账号选择下拉框样式 */ +.file-manager-account-select { + padding-left: 8px !important; + text-indent: 0 !important; + display: flex !important; + align-items: center !important; + line-height: 1.5 !important; + padding-right: 24px !important; +} + +/* 账号选择框右侧圆角 - 需要更高优先级覆盖file-manager-input的圆角设置 */ +.file-manager-rule-bar .file-manager-account-select, +.file-manager-rule-bar-responsive .file-manager-account-select { + border-top-right-radius: 6px !important; + border-bottom-right-radius: 6px !important; +} + +/* 禁止在表格中选择文本,以便更好地支持点击选择 */ +table.selectable-files { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +/* 确保展开按钮在选中状态下仍然可见 */ +tr.selected-file .expand-button { + z-index: 2; +} + +/* 当鼠标悬停在展开按钮上时,不改变按钮的背景色 */ +table.selectable-files .expand-button:hover { + background-color: transparent !important; +} + +/* 文件整理页面重命名按钮悬停效果 */ +table.selectable-files .rename-record-btn:hover { + color: #0A42CC !important; +} + +/* 文件整理页面删除按钮悬停效果 */ +table.selectable-files .delete-record-btn:hover { + color: #b02a37 !important; +} + +/* 选中行或鼠标悬停行的大小列样式 */ +tr.selected-file .file-size-cell .file-size-value, +.selectable-files tbody tr:hover .file-size-cell .file-size-value { + display: none; /* 隐藏文件大小信息 */ +} + +tr.selected-file .file-size-cell .delete-record-btn, +.selectable-files tbody tr:hover .file-size-cell .delete-record-btn { + display: flex; + justify-content: flex-start; /* 居左对齐 */ + align-items: center; + width: auto; + height: 100%; + margin-left: 0; /* 确保没有左边距 */ + padding-left: 0; /* 确保没有左内边距 */ + left: 9px; /* 与大小值保持相同的左边距 */ + position: absolute; /* 使用绝对定位 */ + top: 50%; /* 垂直居中 */ + transform: translateY(-50%); /* 确保垂直居中 */ +} + +/* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */ +table.selectable-files .expand-button:hover { + background-color: #fff !important; /* 保持展开按钮原有的白色背景 */ +} + +/* 文件整理页面表头样式,与转存记录页面保持一致 */ +.selectable-files th, +table.selectable-files th { + vertical-align: middle; + border-bottom: 1px solid var(--border-color) !important; /* 修改底部边框为1px */ + border-top: 1px solid var(--border-color) !important; /* 添加上边框线 */ + padding: 8px 9px !important; /* 统一表头内边距 */ + background-color: var(--button-gray-background-color) !important; /* 表头背景色 */ + color: var(--dark-text-color) !important; /* 表头文字颜色 */ + height: 40px !important; /* 确保表头高度为40px */ + line-height: 24px !important; /* 设置行高以确保文字垂直居中 */ + box-sizing: border-box !important; /* 确保边框包含在总高度内 */ +} + +/* 文件整理页面表格单元格样式,与转存记录页面保持一致 */ +.selectable-files td, +table.selectable-files td { + vertical-align: middle !important; /* 统一使用垂直居中对齐 */ + height: 40px !important; /* 与表头保持一致的高度 */ + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 8px 9px !important; /* 统一单元格内边距 */ + border-bottom: 1px solid var(--border-color); /* 单元格分割线颜色 */ + border-top: none !important; /* 确保没有上边框 */ + box-sizing: border-box !important; /* 确保边框计入总高度 */ + line-height: 24px !important; /* 与表头保持一致的行高 */ +} + +/* 确保文件大小列的垂直对齐保持一致 */ +.table .file-size-cell, +.selectable-files .file-size-cell, +table.selectable-files .file-size-cell { + vertical-align: middle !important; +} + +/* 表头悬停样式 */ +.table thead th.cursor-pointer:hover { + background-color: #f7f7fa; /* 表头悬停背景色 */ +} + +/* 确保所有表格的表头悬停样式一致 */ +.table th.cursor-pointer:hover, +.selectable-files th.cursor-pointer:hover, +table.selectable-files th.cursor-pointer:hover, +table.selectable-records th.cursor-pointer:hover { + background-color: #f7f7fa !important; /* 表头悬停背景色 */ + cursor: pointer; +} + +/* 添加表头排序列的鼠标指针样式 */ +.table th.sortable, +.selectable-files th.sortable, +table.selectable-files th.sortable, +table.selectable-records th.sortable { + cursor: pointer; +} + +/* 确保展开后其他列(如转存日期、大小、修改日期)的内容保持在原始位置 */ +.table tr td { + vertical-align: top !important; /* 确保所有单元格内容顶部对齐 */ +} + +/* 确保文件整理页面和模态框中的表格也应用相同的规则 */ +#fileSelectModal .table tr td, +.selectable-files tr td, +table.selectable-files tr td, +table.selectable-records tr td { + vertical-align: top !important; /* 确保所有单元格内容顶部对齐 */ +} + +/* 确保展开后的行中所有单元格内容保持原位 */ +.table tr:has(.expanded-text) td, +#fileSelectModal .table tr:has([style*="white-space: normal"]) td, +.selectable-files tr:has([style*="white-space: normal"]) td { + vertical-align: top !important; /* 确保展开后所有单元格内容顶部对齐 */ +} + + + +/* 文件大小列中展开行的删除按钮特殊处理 */ +tr:has(.expanded-text) .file-size-cell .delete-record-btn, +.selectable-records tbody tr:has(.expanded-text) .file-size-cell .delete-record-btn, +#fileSelectModal .table tr:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn, +.selectable-files tr:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn { + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 确保不使用任何转换 */ + left: 32px !important; /* 确保左边距固定,在重命名按钮右侧 */ + /* 不设置display,保持默认的隐藏状态 */ +} + +/* 文件大小列中展开行的重命名按钮特殊处理 */ +tr:has(.expanded-text) .file-size-cell .rename-record-btn, +.selectable-records tbody tr:has(.expanded-text) .file-size-cell .rename-record-btn, +#fileSelectModal .table tr:has([style*="white-space: normal"]) .file-size-cell .rename-record-btn, +.selectable-files tr:has([style*="white-space: normal"]) .file-size-cell .rename-record-btn { + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 确保不使用任何转换 */ + left: 5px !important; /* 确保左边距固定 */ + /* 不设置display,保持默认的隐藏状态 */ +} + +/* 修复删除按钮位置问题 - 使用更强制的方法 */ +.delete-record-btn { + color: #dc3545; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: flex-start; + width: 24px; + height: 24px; + border-radius: 4px; + transition: background-color 0.2s ease; + visibility: hidden; + position: absolute !important; /* 强制绝对定位 */ + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 强制禁用任何转换 */ + left: 0; +} + +/* 删除按钮图标大小 */ +.delete-record-btn .bi-trash3 { + font-size: 1rem; +} + +/* 选中行或鼠标悬停行时显示删除按钮 */ +tr.selected-record .delete-record-btn, +.selectable-records tbody tr:hover .delete-record-btn { + visibility: visible; +} + +/* 表头中的删除按钮仅在有选中行时显示 */ +table th .delete-record-btn { + visibility: hidden; +} + +/* 选中行或鼠标悬停行的大小列样式 */ +tr.selected-record .file-size-cell .file-size-value, +.selectable-records tbody tr:hover .file-size-cell .file-size-value { + display: none; /* 隐藏文件大小信息 */ +} + +/* 文件大小列中的删除按钮特殊处理 - 完全重写这部分规则 */ +tr .file-size-cell .delete-record-btn, +tr.selected-record .file-size-cell .delete-record-btn, +.selectable-records tbody tr .file-size-cell .delete-record-btn, +.selectable-records tbody tr:hover .file-size-cell .delete-record-btn { + display: flex; + justify-content: flex-start; + align-items: center; + width: auto; + height: 24px; + margin-left: 0; + padding-left: 0; + left: 9px !important; /* 强制固定左边距 */ + position: absolute !important; /* 强制绝对定位 */ + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 强制禁用任何转换 */ +} + + + + +/* 文件整理页面表格行悬停样式 */ +.selectable-files tbody tr:hover { + background-color: var(--button-gray-background-color); +} + +/* --------------- 文件整理页面样式 --------------- */ + +/* 重命名按钮样式 */ +.selectable-files .rename-record-btn { + color: var(--focus-border-color); + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 4px; + position: absolute; + left: 5px; /* 靠左放置 */ + top: 50%; + transform: translateY(-50%); + font-size: 14px; +} + +/* 文件整理页面的重命名文件按钮图标大小 */ +.selectable-files .rename-record-btn .bi-pencil { + font-size: 0.98rem; + position: relative; + left: 0.5px; +} + +/* 删除按钮样式调整 */ +.selectable-files .delete-record-btn { + color: #dc3545; + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 4px; + position: absolute; + right: auto; /* 移除右对齐 */ + left: 32px; /* 在重命名按钮右侧,调整间距 */ + top: 50%; + transform: translateY(-50%); + font-size: 14px; +} + +/* 文件大小单元格需要相对定位以容纳绝对定位的按钮 */ +.selectable-files .file-size-cell { + position: relative; + min-width: 80px; +} + +/* 修复:确保在悬停和选中状态下重命名和删除按钮显示 */ +.selectable-files tr:hover .file-size-cell .rename-record-btn, +.selectable-files tr.selected-file .file-size-cell .rename-record-btn, +.selectable-files tr:hover .file-size-cell .delete-record-btn, +.selectable-files tr.selected-file .file-size-cell .delete-record-btn { + display: inline-flex !important; + visibility: visible !important; +} + +/* 修复:确保在悬停和选中状态下文件大小值隐藏 */ +.selectable-files tr:hover .file-size-cell .file-size-value, +.selectable-files tr.selected-file .file-size-cell .file-size-value { + display: none !important; + visibility: hidden !important; +} + +/* 重命名输入框样式 - 使用更高优先级 */ +.selectable-files .rename-input { + flex: 1 !important; + min-width: 0 !important; + font-size: 0.94rem !important; + padding: 4px 5.5px !important; + border: 1px solid var(--focus-border-color) !important; + border-radius: 6px !important; + outline: none !important; + background-color: #fff !important; + height: auto !important; + line-height: 1.2 !important; + box-sizing: border-box !important; + margin-top: -2px !important; + margin-bottom: -2px !important; +} + +.selectable-files .rename-input:focus { + border-color: var(--focus-border-color) !important; + box-shadow: none !important; +} + +/* 确保文件整理页面的重命名按钮在悬停和选中状态下始终可见 - 最高优先级 */ +body .selectable-files tbody tr:hover .file-size-cell .rename-record-btn, +body .selectable-files tr.selected-file .file-size-cell .rename-record-btn { + display: inline-flex !important; + visibility: visible !important; + position: absolute !important; + top: 50% !important; + transform: translateY(-50%) !important; + left: 5px !important; + width: 24px !important; + height: 24px !important; + align-items: center !important; + justify-content: center !important; + z-index: 5 !important; + opacity: 1 !important; +} + +/* 确保文件整理页面的删除按钮在悬停和选中状态下始终可见 - 最高优先级 */ +body .selectable-files tbody tr:hover .file-size-cell .delete-record-btn, +body .selectable-files tr.selected-file .file-size-cell .delete-record-btn { + display: inline-flex !important; + visibility: visible !important; + position: absolute !important; + top: 50% !important; + transform: translateY(-50%) !important; + left: 32px !important; + width: 24px !important; + height: 24px !important; + align-items: center !important; + justify-content: center !important; + z-index: 5 !important; + opacity: 1 !important; +} + +/* 展开状态下的重命名按钮悬停和选中状态 - 最高优先级 */ +body .selectable-files tbody tr:hover:has([style*="white-space: normal"]) .file-size-cell .rename-record-btn, +body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .file-size-cell .rename-record-btn { + display: inline-flex !important; + visibility: visible !important; + position: absolute !important; + top: 8px !important; + transform: none !important; + left: 5px !important; + width: 24px !important; + height: 24px !important; + align-items: center !important; + justify-content: center !important; + z-index: 5 !important; + opacity: 1 !important; +} + +/* 展开状态下的删除按钮悬停和选中状态 - 最高优先级 */ +body .selectable-files tbody tr:hover:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn, +body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn { + display: inline-flex !important; + visibility: visible !important; + position: absolute !important; + top: 8px !important; + transform: none !important; + left: 32px !important; + width: 24px !important; + height: 24px !important; + align-items: center !important; + justify-content: center !important; + z-index: 5 !important; + opacity: 1 !important; +} + +/* 文件整理页面的文件名单元格样式 */ +.selectable-files td .text-truncate { + display: block; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 25px; /* 为展开按钮预留空间 */ + line-height: inherit; /* 使用继承的行高 */ +} + +/* 文件整理页面展开后的文本样式 */ +.selectable-files td div:not(.text-truncate):not(.expand-button):not(.d-flex) { + white-space: normal; + word-break: break-all; + display: block; + line-height: inherit; /* 使用继承的行高 */ + margin-top: 0; + margin-bottom: -1px; + position: relative; + padding-right: 25px; + max-width: 100%; /* 确保不超出单元格宽度 */ + top: -1px; +} + +/* 展开按钮悬停状态 */ +.selectable-files .expand-button:hover { + background-color: #fff !important; /* 保持白色背景 */ + opacity: 1 !important; +} + +/* 展开按钮样式 */ +.selectable-files .expand-button { + position: absolute; + right: 5px; + top: 11px; /* 固定位置,不使用垂直居中 */ + transform: none; /* 移除垂直居中转换 */ + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; + background: #fff; + border-radius: 50%; + width: 18px; + height: 18px; + text-align: center; + line-height: 18px; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); + z-index: 2; + pointer-events: auto; /* 确保点击事件不被阻止 */ +} + +/* 展开按钮悬停时可见 */ +.selectable-files .position-relative:hover .expand-button { + opacity: 1; +} + +/* 展开按钮中的图标样式 */ +.selectable-files .expand-button .bi { + font-size: 0.7em; + vertical-align: middle; + position: relative; + top: -1px; +} + +/* 确保单元格垂直居中 */ +.selectable-files td { + vertical-align: middle !important; +} + +/* 确保图标与文本垂直对齐 */ +.selectable-files .d-flex.align-items-center { + align-items: center !important; +} + +/* 确保图标固定大小不变形 */ +.selectable-files .d-flex.align-items-center i.bi { + flex-shrink: 0; +} + +/* 确保图标与文本垂直对齐 - 使用固定高度 */ +.selectable-files .d-flex.align-items-center { + align-items: flex-start !important; /* 改为顶部对齐,使用固定高度 */ +} + +/* 确保图标固定大小不变形,使用固定位置 */ +.selectable-files .d-flex.align-items-center i.bi { + flex-shrink: 0; + position: relative; + top: 0px; /* 固定图标位置,向下移动4px */ +} + +/* 修复文件整理页面展开后文件大小列的垂直对齐问题 */ +/* 确保展开后的行中所有单元格都使用顶部对齐 */ +.selectable-files tr:has([style*="white-space: normal"]) td, +.selectable-files tr:has(div:not(.text-truncate):not(.expand-button):not(.d-flex)) td { + vertical-align: top !important; /* 强制展开后所有单元格顶部对齐 */ +} + +/* 特别处理文件大小列,确保展开后也使用顶部对齐 */ +.selectable-files tr:has([style*="white-space: normal"]) .file-size-cell, +.selectable-files tr:has(div:not(.text-truncate):not(.expand-button):not(.d-flex)) .file-size-cell { + vertical-align: top !important; /* 强制文件大小列顶部对齐 */ +} + + + +/* 确保选中状态下文件整理页面的展开按钮也保持白色背景 */ +.selectable-files .expand-button { + background-color: #fff !important; +} + +/* 文件整理页面重命名配置框输入框样式 - 去除圆角 */ +.file-manager-rule-bar .file-manager-input { + border-radius: 0 !important; /* 去除圆角 */ +} + +/* 账号选择框例外 - 保持右侧圆角 */ +.file-manager-rule-bar .file-manager-input.file-manager-account-select { + border-top-right-radius: 6px !important; + border-bottom-right-radius: 6px !important; +} + +/* 文件整理页面重命名配置框输入框相邻边框重叠样式 */ +.file-manager-rule-bar .file-manager-input:not(:first-child) { + margin-left: -1px; /* 向左移动1px,使边框重叠 */ +} + +/* 确保输入框在输入组中的边框重叠 */ +.file-manager-rule-bar .input-group .file-manager-input:not(:first-child) { + border-left: 1px solid var(--border-color) !important; /* 确保左边框显示 */ + margin-left: -1px; /* 向左移动1px,使边框重叠 */ +} + +/* 文件整理页面正则命名按钮样式 - 保持左边圆角,去除右边圆角 */ +.file-manager-rule-bar .input-group-prepend .btn { + position: relative; /* 添加相对定位以应用z-index */ + z-index: 2; /* 确保边框在顶层 */ + border-top-left-radius: 6px !important; /* 保持左上角圆角 */ + border-bottom-left-radius: 6px !important; /* 保持左下角圆角 */ + border-top-right-radius: 0 !important; /* 去除右上角圆角 */ + border-bottom-right-radius: 0 !important; /* 去除右下角圆角 */ + border-right: 1px solid var(--dark-text-color) !important; /* 确保右边框显示 */ +} + +/* 文件整理页面匹配表达式输入框左边框与正则命名按钮右边框重叠 */ +.file-manager-rule-bar .input-group-prepend + input.file-manager-input { + margin-left: 0px; +} + +/* 文件整理页面激活的输入框边框置顶 */ +.file-manager-rule-bar .file-manager-input:focus { + position: relative; + z-index: 3; +} + +/* 仅影响文件选择模态框表格的表头和单元格高度,减少5px */ +#fileSelectModal .table th { + padding-top: 4.5px !important; /* 原7px,减少2.5px */ + padding-bottom: 4px !important; /* 原6.5px,减少2.5px */ + height: 35px !important; /* 原40px,减少5px */ + line-height: 19px !important; /* 原24px,减少5px */ +} + +#fileSelectModal .table td { + padding-top: 3px !important; /* 原5.5px,减少2.5px */ + padding-bottom: 4.5px !important; /* 原7px,减少2.5px */ + height: 35px !important; /* 原40px,减少5px */ + line-height: 19px !important; /* 原24px,减少5px */ +} + +#fileSelectModal .table td { + vertical-align: middle !important; + position: relative; +} + +#fileSelectModal .table td > *:not(.expand-button) { + position: relative; + top: 3.5px; +} + +#fileSelectModal .table td.col-size, +#fileSelectModal .table td.col-date { + padding-top: 6px !important; +} + +#fileSelectModal .table td.col-rename, +#fileSelectModal .table td.col-action { + padding-top: 2.5px !important; +} + +#fileSelectModal[data-modal-type="preview"] .table td.col-rename { + padding-top: 3px !important; +} + +/* 文件整理页面的文件图标样式 */ +.selectable-files .bi-file-earmark { + font-size: 1.06rem; /* 比模态框的0.95rem大一些 */ + margin-right: 7px !important; /* 图标距离文本的距离 */ + position: relative; + top: 1px; /* 可微调垂直对齐 */ + left: -1px; /* 可微调水平对齐 */ +} + +/* 文件整理页面的文件夹图标样式 */ +.bi-folder-fill { + font-size: 1.06rem; /* 比模态框的0.95rem大一些 */ + margin-right: 7px !important; /* 图标距离文本的距离 */ + position: relative; + top: 1px; /* 可微调垂直对齐 */ + left: -1px; /* 可微调水平对齐 */ + color: #ffc107; /* 保持黄色 */ +} + +/* 文件整理页面无法识别剧集编号样式 */ +#fileSelectModal[data-modal-type="preview-filemanager"] .episode-number-text { + position: relative; + top: 1.5px; /* 或你想要的像素 */ + display: inline-block; +} + +/* 任务配置页面无法识别剧集编号样式 */ +#fileSelectModal[data-modal-type="preview"] .episode-number-text { + position: relative; + top: 1px; + display: inline-block; + left: 2px; +} + +/* 文件整理页面无法识别剧集编号前面的 × 样式 */ +#fileSelectModal[data-modal-type="preview-filemanager"] .episode-x { + position: relative; + top: 0.5px; + display: inline-block; + margin-right: 2px; +} + +/* 文件整理页面命名预览模式下的绿色重命名文本上移0.5px */ +#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename.text-success > * { + position: relative; + top: 3px !important; /* 原来是3px,上移0.5px */ +} + +/* 文件整理页面命名预览模式下的取消按钮样式 */ +#fileSelectModal[data-modal-type="preview-filemanager"] .modal-footer .btn-cancel { + background-color: var(--button-gray-background-color) !important; + border-color: var(--button-gray-background-color) !important; + color: var(--dark-text-color) !important; +} +#fileSelectModal[data-modal-type="preview-filemanager"] .modal-footer .btn-cancel:hover { + background-color: #e0e2e6 !important; + border-color: #e0e2e6 !important; + color: var(--dark-text-color) !important; +} + +@media (max-width: 767.98px) { + .file-manager-rule-bar-responsive { + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + } + .file-manager-rule-bar-responsive .rule-row { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 8px; + gap: 8px; + width: 100%; + } + .file-manager-rule-bar-responsive .rule-row:last-child { + margin-bottom: 0; + } + .file-manager-rule-bar-responsive .rule-row input, + .file-manager-rule-bar-responsive .rule-row button, + .file-manager-rule-bar-responsive .rule-row .input-group-text, + .file-manager-rule-bar-responsive .rule-row label { + flex: 1 1 0; + min-width: 0; + } + .file-manager-rule-bar-responsive .rule-row .input-group-prepend, + .file-manager-rule-bar-responsive .rule-row .input-group-text { + flex: 0 0 auto; + } + .file-manager-rule-bar-responsive .filter-label { + flex: 0 0 auto; + margin-right: 4px; + white-space: nowrap; + } +} + +.file-manager-rule-bar-responsive { display: none; } +.file-manager-rule-bar { display: flex; } +@media (max-width: 767.98px) { + .file-manager-rule-bar { display: none !important; } + .file-manager-rule-bar-responsive { display: block !important; } + .file-manager-rule-bar-responsive .input-group { width: 100%; } + .file-manager-rule-bar-responsive .input-group + .input-group { margin-top: 8px; } + + /* 移动端账号选择栏下边距 - 确保与命名规则栏有8px间距 */ + .file-manager-account-selector { + margin-bottom: 8px !important; + } + + .file-manager-account-selector .file-manager-rule-bar-responsive { + margin-bottom: 0 !important; /* 移除内部边距,使用外部容器的边距 */ + } + /* 含文件夹样式调整 */ + .file-manager-rule-bar-responsive .input-group-text.file-folder-rounded { + border-top-left-radius: 0 !important; /* 左侧不要圆角 */ + border-bottom-left-radius: 0 !important; /* 左侧不要圆角 */ + border-top-right-radius: 6px !important; /* 右侧圆角6px */ + border-bottom-right-radius: 6px !important; /* 右侧圆角6px */ + margin-left: -1px; /* 左边框与过滤规则输入框重叠 */ + position: relative; + z-index: 1; /* 确保边框显示正确 */ + } + + /* 过滤规则输入框在含文件夹前面时去除右侧圆角 */ + .file-manager-rule-bar-responsive .input-group .form-control + .input-group-text.file-folder-rounded { + border-left: 1px solid #ced4da; /* 确保左边框显示 */ + } + + /* 过滤规则输入框右侧圆角调整 */ + .file-manager-rule-bar-responsive .input-group .form-control:not(:last-child) { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + /* 预览并执行重命名按钮与含文件夹间距8px,且与上方input-group间距8px */ + .file-manager-rule-bar-responsive .input-group-append .btn { + margin-left: 8px; + } + /* 只影响移动端的“预览并执行重命名”按钮上边距 */ + .file-manager-rule-bar-responsive .batch-rename-btn { + margin-top: 0px; /* 你想要的上边距 */ + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px !important; + } + + /* 移动端预览并执行重命名按钮与含文件夹间距 */ + .file-manager-rule-bar-responsive .input-group .batch-rename-btn { + margin-left: 8px !important; + } + + /* 移动端账号选择栏中的按钮间距 */ + .file-manager-rule-bar-responsive .d-flex .batch-rename-btn:first-of-type { + margin-left: 8px !important; + } + + .file-manager-rule-bar-responsive .d-flex .batch-rename-btn + .batch-rename-btn { + margin-left: 8px !important; + } +} +@media (min-width: 768px) { + .file-manager-rule-bar { display: flex !important; } + .file-manager-rule-bar-responsive { display: none !important; } +} diff --git a/app/static/js/sort_file_by_name.js b/app/static/js/sort_file_by_name.js new file mode 100644 index 0000000..4be5773 --- /dev/null +++ b/app/static/js/sort_file_by_name.js @@ -0,0 +1,155 @@ +// 与后端 quark_auto_save.py 的 sort_file_by_name 完全一致的排序逻辑 +// 用于前端文件列表排序 + +function chineseToArabic(chinese) { + // 简单实现,支持一到一万 + const cnNums = { + '零': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, + '十': 10, '百': 100, '千': 1000, '万': 10000 + }; + let result = 0, unit = 1, num = 0; + for (let i = chinese.length - 1; i >= 0; i--) { + const char = chinese[i]; + if (cnNums[char] >= 10) { + unit = cnNums[char]; + if (unit === 10 && (i === 0 || cnNums[chinese[i - 1]] === undefined)) { + num = 1; + } + } else if (cnNums[char] !== undefined) { + num = cnNums[char]; + result += num * unit; + } + } + return result || null; +} + +function sortFileByName(file) { + // 兼容 dict 或字符串 + let filename = typeof file === 'object' ? (file.file_name || '') : file; + let update_time = typeof file === 'object' ? (file.updated_at || 0) : 0; + let file_name_without_ext = filename.replace(/\.[^/.]+$/, ''); + let date_value = Infinity, episode_value = Infinity, segment_value = 0; + + // 1. 日期提取 + let match; + // YYYY-MM-DD + match = filename.match(/((?:19|20)\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/); + if (match) { + date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]); + } + // YY-MM-DD + if (date_value === Infinity) { + match = filename.match(/((?:19|20)?\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/); + if (match && match[1].length === 2) { + let year = parseInt('20' + match[1]); + date_value = year * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]); + } + } + // YYYYMMDD + if (date_value === Infinity) { + match = filename.match(/((?:19|20)\d{2})(\d{2})(\d{2})/); + if (match) { + date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]); + } + } + // YYMMDD + if (date_value === Infinity) { + match = filename.match(/(? 12) [month, day] = [day, month]; + date_value = year * 10000 + month * 100 + day; + } + } + // MM-DD + if (date_value === Infinity) { + match = filename.match(/(? 12) [month, day] = [day, month]; + date_value = 20000000 + month * 100 + day; + } + } + + // 2. 期数/集数 + // 第X期/集/话 + match = filename.match(/第(\d+)[期集话]/); + if (match) episode_value = parseInt(match[1]); + // 第[中文数字]期/集/话 + if (episode_value === Infinity) { + match = filename.match(/第([一二三四五六七八九十百千万零两]+)[期集话]/); + if (match) { + let arabic = chineseToArabic(match[1]); + if (arabic !== null) episode_value = arabic; + } + } + // X集/期/话 + if (episode_value === Infinity) { + match = filename.match(/(\d+)[期集话]/); + if (match) episode_value = parseInt(match[1]); + } + // [中文数字]集/期/话 + if (episode_value === Infinity) { + match = filename.match(/([一二三四五六七八九十百千万零两]+)[期集话]/); + if (match) { + let arabic = chineseToArabic(match[1]); + if (arabic !== null) episode_value = arabic; + } + } + // S01E01 + if (episode_value === Infinity) { + match = filename.match(/[Ss](\d+)[Ee](\d+)/); + if (match) episode_value = parseInt(match[2]); + } + // E01/EP01 + if (episode_value === Infinity) { + match = filename.match(/[Ee][Pp]?(\d+)/); + if (match) episode_value = parseInt(match[1]); + } + // 1x01 + if (episode_value === Infinity) { + match = filename.match(/(\d+)[Xx](\d+)/); + if (match) episode_value = parseInt(match[2]); + } + // [数字]或【数字】 + if (episode_value === Infinity) { + match = filename.match(/\[(\d+)\]|【(\d+)】/); + if (match) episode_value = parseInt(match[1] || match[2]); + } + // 纯数字文件名 + if (episode_value === Infinity) { + if (/^\d+$/.test(file_name_without_ext)) { + episode_value = parseInt(file_name_without_ext); + } else { + match = filename.match(/(\d+)/); + if (match) episode_value = parseInt(match[1]); + } + } + + // 3. 上中下 + if (/[上][集期话部篇]?|[集期话部篇]上/.test(filename)) segment_value = 1; + else if (/[中][集期话部篇]?|[集期话部篇]中/.test(filename)) segment_value = 2; + else if (/[下][集期话部篇]?|[集期话部篇]下/.test(filename)) segment_value = 3; + + return [date_value, episode_value, segment_value, update_time]; +} + +// 用法: +// arr.sort((a, b) => { +// const ka = sortFileByName(a), kb = sortFileByName(b); +// for (let i = 0; i < ka.length; ++i) { +// if (ka[i] !== kb[i]) return ka[i] > kb[i] ? 1 : -1; +// } +// return 0; +// }); \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 3ee4995..32a772d 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -16,6 +16,7 @@ + diff --git a/plugins/alist.py b/plugins/alist.py index 6dd24e2..6f3e013 100644 --- a/plugins/alist.py +++ b/plugins/alist.py @@ -16,6 +16,9 @@ class Alist: # 缓存参数 storage_mount_path = None quark_root_dir = None + # 多账号支持 + storage_mount_paths = [] + quark_root_dirs = [] def __init__(self, **kwargs): """初始化AList插件""" @@ -28,8 +31,17 @@ class Alist: if key in kwargs: setattr(self, key, kwargs[key]) else: - print(f"{self.plugin_name} 模块缺少必要参数: {key}") - + pass # 不显示缺少参数的提示 + + # 处理多账号配置:支持数组形式的storage_id + if isinstance(self.storage_id, list): + self.storage_ids = self.storage_id + # 为了向后兼容,使用第一个ID作为默认值 + self.storage_id = self.storage_ids[0] if self.storage_ids else "" + else: + # 单一配置转换为数组格式 + self.storage_ids = [self.storage_id] if self.storage_id else [] + # 检查基本配置 if not self.url or not self.token or not self.storage_id: return @@ -43,27 +55,56 @@ class Alist: # 验证AList连接 if self.get_info(): - # 解析存储ID - success, result = self.storage_id_to_path(self.storage_id) - if success: - self.storage_mount_path, self.quark_root_dir = result - - # 确保路径格式正确 - if self.quark_root_dir != "/": - if not self.quark_root_dir.startswith("/"): - self.quark_root_dir = f"/{self.quark_root_dir}" - self.quark_root_dir = self.quark_root_dir.rstrip("/") - - if not self.storage_mount_path.startswith("/"): - self.storage_mount_path = f"/{self.storage_mount_path}" - self.storage_mount_path = self.storage_mount_path.rstrip("/") - + # 解析所有存储ID + for i, storage_id in enumerate(self.storage_ids): + success, result = self.storage_id_to_path(storage_id) + if success: + mount_path, root_dir = result + + # 确保路径格式正确 + if root_dir != "/": + if not root_dir.startswith("/"): + root_dir = f"/{root_dir}" + root_dir = root_dir.rstrip("/") + + if not mount_path.startswith("/"): + mount_path = f"/{mount_path}" + mount_path = mount_path.rstrip("/") + + self.storage_mount_paths.append(mount_path) + self.quark_root_dirs.append(root_dir) + + if i == 0: + # 设置默认值(向后兼容) + self.storage_mount_path = mount_path + self.quark_root_dir = root_dir + + + else: + print(f"AList 刷新: 存储ID [{i}] {storage_id} 解析失败") + # 添加空值保持索引对应 + self.storage_mount_paths.append("") + self.quark_root_dirs.append("") + + # 只要有一个存储ID解析成功就激活插件 + if any(self.storage_mount_paths): self.is_active = True else: - print(f"AList 刷新: 存储信息解析失败") + print(f"AList 刷新: 所有存储ID解析失败") else: print(f"AList 刷新: 服务器连接失败") + def get_storage_config(self, account_index=0): + """根据账号索引获取对应的存储配置""" + if account_index < len(self.storage_mount_paths) and account_index < len(self.quark_root_dirs): + return self.storage_mount_paths[account_index], self.quark_root_dirs[account_index] + else: + # 如果索引超出范围,使用第一个配置作为默认值 + if self.storage_mount_paths and self.quark_root_dirs: + return self.storage_mount_paths[0], self.quark_root_dirs[0] + else: + return "", "" + def run(self, task, **kwargs): """ 插件主入口,当有新文件保存时触发刷新AList目录 diff --git a/plugins/alist_strm_gen.py b/plugins/alist_strm_gen.py index 295c6ee..d39948c 100644 --- a/plugins/alist_strm_gen.py +++ b/plugins/alist_strm_gen.py @@ -46,12 +46,10 @@ class Alist_strm_gen: missing_configs.append(key) if missing_configs: - print(f"{self.plugin_name} 模块缺少必要参数: {', '.join(missing_configs)}") - return - + return # 不显示缺少参数的提示 + if not self.url or not self.token or not self.storage_id: - print(f"{self.plugin_name} 模块配置不完整,请检查配置") - return + return # 不显示配置不完整的提示 # 检查 strm_save_dir 是否存在 if not os.path.exists(self.strm_save_dir): diff --git a/plugins/plex.py b/plugins/plex.py index b26a528..69a25a3 100644 --- a/plugins/plex.py +++ b/plugins/plex.py @@ -18,11 +18,29 @@ class Plex: if key in kwargs: setattr(self, key, kwargs[key]) else: - print(f"{self.__class__.__name__} 模块缺少必要参数: {key}") + pass # 不显示缺少参数的提示 + + # 处理多账号配置:支持数组形式的quark_root_path + if isinstance(self.quark_root_path, list): + self.quark_root_paths = self.quark_root_path + # 为了向后兼容,使用第一个路径作为默认值 + self.quark_root_path = self.quark_root_paths[0] if self.quark_root_paths else "" + else: + # 单一配置转换为数组格式 + self.quark_root_paths = [self.quark_root_path] if self.quark_root_path else [] + if self.url and self.token and self.quark_root_path: if self.get_info(): self.is_active = True + def get_quark_root_path(self, account_index=0): + """根据账号索引获取对应的quark_root_path""" + if account_index < len(self.quark_root_paths): + return self.quark_root_paths[account_index] + else: + # 如果索引超出范围,使用第一个路径作为默认值 + return self.quark_root_paths[0] if self.quark_root_paths else "" + def run(self, task, **kwargs): if task.get("savepath"): # 检查是否已缓存库信息 @@ -59,10 +77,8 @@ class Plex: try: for library in self._libraries: for location in library.get("Location", []): - if ( - os.path.commonpath([folder_path, location["path"]]) - == location["path"] - ): + location_path = location.get("path", "") + if folder_path.startswith(location_path): refresh_url = f"{self.url}/library/sections/{library['key']}/refresh?path={folder_path}" refresh_response = requests.get(refresh_url, headers=headers) if refresh_response.status_code == 200: diff --git a/quark_auto_save.py b/quark_auto_save.py index 842ec15..21cfabb 100644 --- a/quark_auto_save.py +++ b/quark_auto_save.py @@ -229,6 +229,9 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None): Returns: int: 提取到的剧集号,如果无法提取则返回None """ + # 首先去除文件扩展名 + file_name_without_ext = os.path.splitext(filename)[0] + # 预处理:排除文件名中可能是日期的部分,避免误识别 date_patterns = [ # YYYY-MM-DD 或 YYYY.MM.DD 或 YYYY/MM/DD 或 YYYY MM DD格式(四位年份) @@ -245,10 +248,10 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None): r'(?