From 6f68c5a290c9483e39ddfe9945b3737bc19efbe3 Mon Sep 17 00:00:00 2001 From: x1ao4 Date: Sun, 29 Jun 2025 02:26:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=E6=96=87=E4=BB=B6=E6=95=B4=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=88=B7=E6=96=B0?= =?UTF-8?q?=20Plex=20=E5=AA=92=E4=BD=93=E5=BA=93=E5=92=8C=E5=88=B7?= =?UTF-8?q?=E6=96=B0=20AList=20=E7=9B=AE=E5=BD=95=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E4=B8=BA=20Plex=20=E5=92=8C=20AList=20=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=A4=9A=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/run.py | 179 ++++++++++++++++++++++++++++++++++++++ app/static/css/main.css | 104 ++++++++++++++++++++-- app/templates/index.html | 89 +++++++++++++++++-- plugins/alist.py | 77 ++++++++++++---- plugins/alist_strm_gen.py | 8 +- plugins/plex.py | 26 ++++-- 6 files changed, 444 insertions(+), 39 deletions(-) diff --git a/app/run.py b/app/run.py index ef08451..88b6aa9 100644 --- a/app/run.py +++ b/app/run.py @@ -241,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"], @@ -307,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(): @@ -320,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}) @@ -441,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(): diff --git a/app/static/css/main.css b/app/static/css/main.css index 50a8d7c..0a43c92 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -3965,14 +3965,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; @@ -4201,11 +4201,32 @@ select.task-filter-select, } .batch-rename-btn { - margin-left: 8px; + 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; @@ -4217,6 +4238,50 @@ select.task-filter-select, font-size: 1.15rem; } +/* 确保文件整理页面的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; @@ -4274,8 +4339,24 @@ select.task-filter-select, } .batch-rename-btn { - margin-left: 0; 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 { @@ -5126,6 +5207,13 @@ body .selectable-files tr.selected-file .file-size-cell .delete-record-btn { /* 只影响移动端的“预览并执行重命名”按钮上边距 */ .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; } /* 移动端预览并执行重命名按钮与含文件夹间距 */ @@ -5133,8 +5221,12 @@ body .selectable-files tr.selected-file .file-size-cell .delete-record-btn { margin-left: 8px !important; } - /* 移动端账号选择栏中的刷新按钮间距 */ - .file-manager-rule-bar-responsive .d-flex .batch-rename-btn { + /* 移动端账号选择栏中的按钮间距 */ + .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; } } diff --git a/app/templates/index.html b/app/templates/index.html index 771bcc6..42b1af7 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -451,9 +451,11 @@
- +
- +
@@ -1047,6 +1049,8 @@ + + @@ -1062,6 +1066,8 @@ + + @@ -1946,7 +1952,27 @@ return message; }, - + + // 获取插件配置的占位符文本 + getPluginConfigPlaceholder(pluginName, key) { + if (pluginName === 'plex' && key === 'quark_root_path') { + return '输入夸克根目录相对于 Plex 媒体库目录的路径,多个路径用逗号分隔'; + } else if (pluginName === 'alist' && key === 'storage_id') { + return '输入 AList 服务器夸克存储的 ID,多个 ID 用逗号分隔'; + } + return ''; + }, + + // 获取插件配置的帮助文本 + getPluginConfigHelp(pluginName, key) { + if (pluginName === 'plex' && key === 'quark_root_path') { + return '多账号支持:多个路径用逗号分隔,顺序与Cookie顺序对应,如:/path1, /path2'; + } else if (pluginName === 'alist' && key === 'storage_id') { + return '多账号支持:多个存储ID用逗号分隔,顺序与Cookie顺序对应,如:1, 2, 3'; + } + return ''; + }, + fetchUserInfo() { // 获取所有cookie对应的用户信息 axios.get('/get_user_info') @@ -3093,7 +3119,7 @@ // 短暂延迟后重试 setTimeout(() => { this.getSavepathDetail(params, retryCount + 1, maxRetries); - }, 2000); // 2秒后重试 + }, 1000); // 1秒后重试 } else { // 超过最大重试次数,显示错误信息 this.fileSelect.error = "获取文件夹列表失败,请关闭窗口再试一次"; @@ -3178,7 +3204,7 @@ // 短暂延迟后重试 setTimeout(() => { this.getShareDetail(retryCount + 1, maxRetries); - }, 2000); // 2秒后重试 + }, 1000); // 1秒后重试 } else { // 超过最大重试次数,显示错误信息 this.fileSelect.error = "获取文件夹列表失败,请关闭窗口再试一次"; @@ -4440,6 +4466,59 @@ alert("刷新 AList 目录失败: " + (error.response?.data?.message || error.message || "未知错误")); }); }, + // 文件整理页面刷新Plex媒体库 + refreshFileManagerPlexLibrary() { + // 获取当前目录路径 + const currentPath = this.getCurrentFolderPath(); + + axios.post('/refresh_filemanager_plex_library', { + folder_path: currentPath, + account_index: this.fileManager.selectedAccountIndex + }) + .then(response => { + if (response.data.success) { + this.showToast(response.data.message); + } else { + alert(response.data.message); + } + }) + .catch(error => { + alert("刷新 Plex 媒体库失败: " + (error.response?.data?.message || error.message || "未知错误")); + }); + }, + // 文件整理页面刷新AList目录 + refreshFileManagerAlistDirectory() { + // 获取当前目录路径 + const currentPath = this.getCurrentFolderPath(); + + axios.post('/refresh_filemanager_alist_directory', { + folder_path: currentPath, + account_index: this.fileManager.selectedAccountIndex + }) + .then(response => { + if (response.data.success) { + this.showToast(response.data.message); + } else { + alert(response.data.message); + } + }) + .catch(error => { + alert("刷新 AList 目录失败: " + (error.response?.data?.message || error.message || "未知错误")); + }); + }, + // 获取当前文件夹路径(相对于夸克网盘根目录) + getCurrentFolderPath() { + if (this.fileManager.currentFolder === 'root') { + return '/'; // 根目录返回 /,表示夸克网盘根目录 + } + + // 构建完整路径(相对于夸克网盘根目录) + let path = ''; + for (const pathItem of this.fileManager.paths) { + path += '/' + pathItem.name; + } + return path || '/'; + }, filterByTaskName(taskName, event) { // 防止事件冒泡,避免触发行选择 if (event) { 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: