在文件整理页面增加了刷新 Plex 媒体库和刷新 AList 目录功能,为 Plex 和 AList 插件增加了多账号支持功能

This commit is contained in:
x1ao4 2025-06-29 02:26:41 +08:00
parent 7d4672cb8e
commit 6f68c5a290
6 changed files with 444 additions and 39 deletions

View File

@ -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():

View File

@ -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;
}
}

View File

@ -451,9 +451,11 @@
<div class="collapse" :id="'collapse_'+pluginName" style="margin-left: 26px;">
<div v-for="(value, key, keyIndex) in plugin" :key="key" class="input-group mb-2" :style="{ marginBottom: keyIndex === Object.keys(plugin).length - 1 && pluginName === Object.keys(getAvailablePlugins(formData.plugins)).pop() ? '8.5px !important' : '' }">
<div class="input-group-prepend">
<span class="input-group-text" v-html="key"></span>
<span class="input-group-text" v-html="key" :title="getPluginConfigHelp(pluginName, key)"></span>
</div>
<input type="text" v-model="formData.plugins[pluginName][key]" class="form-control">
<input type="text" v-model="formData.plugins[pluginName][key]" class="form-control"
:placeholder="getPluginConfigPlaceholder(pluginName, key)"
:title="getPluginConfigHelp(pluginName, key)">
</div>
</div>
</div>
@ -1047,6 +1049,8 @@
<option v-for="(account, index) in accountsDetail" :key="index" :value="index">{{ account.display_text }}</option>
</select>
</div>
<button type="button" class="btn btn-outline-plex batch-rename-btn" v-if="formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" @click="refreshFileManagerPlexLibrary" title="刷新Plex媒体库"><img src="./static/Plex.svg" class="plex-icon"></button>
<button type="button" class="btn btn-outline-alist batch-rename-btn" v-if="formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" @click="refreshFileManagerAlistDirectory" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="refreshCurrentFolderCache" title="刷新当前目录缓存">
<i class="bi bi-arrow-clockwise"></i>
</button>
@ -1062,6 +1066,8 @@
<option v-for="(account, index) in accountsDetail" :key="index" :value="index">{{ account.display_text }}</option>
</select>
</div>
<button type="button" class="btn btn-outline-plex batch-rename-btn" v-if="formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" @click="refreshFileManagerPlexLibrary" title="刷新Plex媒体库"><img src="./static/Plex.svg" class="plex-icon"></button>
<button type="button" class="btn btn-outline-alist batch-rename-btn" v-if="formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" @click="refreshFileManagerAlistDirectory" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="refreshCurrentFolderCache" title="刷新当前目录缓存">
<i class="bi bi-arrow-clockwise"></i>
</button>
@ -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) {

View File

@ -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目录

View File

@ -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):

View File

@ -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: