import os import re import json import requests import time class Alist: default_config = { "url": "", # Alist服务器URL "token": "", # Alist服务器Token "storage_id": "", # Alist 服务器夸克存储 ID } is_active = False # 缓存参数 storage_mount_path = None quark_root_dir = None # 多账号支持 storage_mount_paths = [] quark_root_dirs = [] def __init__(self, **kwargs): """初始化AList插件""" # 标记插件名称,便于日志识别 self.plugin_name = self.__class__.__name__.lower() if kwargs: # 加载配置 for key, _ in self.default_config.items(): if key in kwargs: setattr(self, key, kwargs[key]) else: 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 # 确保URL格式正确 if not self.url.startswith(("http://", "https://")): self.url = f"http://{self.url}" # 移除URL末尾的斜杠 self.url = self.url.rstrip("/") # 验证AList连接 if self.get_info(): # 解析所有存储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 刷新: 所有存储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目录 Args: task: 任务信息,包含savepath等关键信息 **kwargs: 其他参数,包括tree和rename_logs Returns: task: 返回原任务信息 """ # 检查路径是否在夸克根目录内 if task.get("savepath"): # 确保路径符合要求 quark_path = task.get("savepath", "") if not quark_path.startswith("/"): quark_path = f"/{quark_path}" # 检查路径是否在夸克根目录下,或夸克根目录是否为根目录 if self.quark_root_dir == "/" or quark_path.startswith(self.quark_root_dir): # 映射到AList路径 alist_path = self.map_quark_to_alist_path(quark_path) # 执行刷新 self.refresh(alist_path) return task def get_info(self): """获取AList服务器信息""" url = f"{self.url}/api/admin/setting/list" headers = {"Authorization": self.token} querystring = {"group": "1"} try: response = requests.request("GET", url, headers=headers, params=querystring) response.raise_for_status() response = response.json() if response.get("code") == 200: print(f"AList 刷新: {response.get('data',[])[1].get('value','')} {response.get('data',[])[0].get('value','')}") return True else: print(f"AList 刷新: 连接失败 ❌ {response.get('message')}") except Exception as e: print(f"AList 刷新: 连接出错 ❌ {str(e)}") return False def storage_id_to_path(self, storage_id): """ 将存储ID转换为挂载路径和夸克根目录 Args: storage_id: 存储ID Returns: tuple: (成功状态, (挂载路径, 夸克根目录)) """ storage_mount_path, quark_root_dir = None, None # 1. 检查是否符合 /aaa:/bbb 格式 if match := re.match(r"^(\/[^:]*):(\/[^:]*)$", storage_id): # 存储挂载路径, 夸克根文件夹 storage_mount_path, quark_root_dir = match.group(1), match.group(2) file_list = self.get_file_list(storage_mount_path) if file_list.get("code") != 200: print(f"AList 刷新: 挂载路径无效") return False, (None, None) # 2. 检查是否数字,调用 Alist API 获取存储信息 elif re.match(r"^\d+$", storage_id): if storage_info := self.get_storage_info(storage_id): if storage_info["driver"] == "Quark": addition = json.loads(storage_info["addition"]) # 存储挂载路径 storage_mount_path = storage_info["mount_path"] # 夸克根文件夹 quark_root_dir = self.get_root_folder_full_path( addition["cookie"], addition["root_folder_id"] ) else: print(f"AList 刷新: 不支持 [{storage_info['driver']}] 驱动") else: print(f"AList 刷新: 获取存储信息失败") return False, (None, None) else: print(f"AList 刷新: storage_id 格式错误") return False, (None, None) # 返回结果 if storage_mount_path and quark_root_dir: return True, (storage_mount_path, quark_root_dir) else: return False, (None, None) def get_storage_info(self, storage_id): """获取AList存储详细信息""" url = f"{self.url}/api/admin/storage/get" headers = {"Authorization": self.token} querystring = {"id": storage_id} try: response = requests.request("GET", url, headers=headers, params=querystring) response.raise_for_status() data = response.json() if data.get("code") == 200: return data.get("data", {}) else: print(f"AList 刷新: 获取存储信息失败 ({data.get('message', '未知错误')})") except Exception as e: print(f"AList 刷新: 获取存储信息出错 ({str(e)})") return None def refresh(self, path, retry_count=2): """ 刷新AList目录,支持重试和自动回溯到父目录 Args: path: 需要刷新的路径 retry_count: 重试次数,默认重试2次 """ # 实现重试机制 for attempt in range(retry_count + 1): if attempt > 0: # 不输出重试信息 pass data = self.get_file_list(path, True) if data.get("code") == 200: print(f"📁 刷新 AList 目录: [{path}] 成功 ✅") return data.get("data") elif "object not found" in data.get("message", ""): # 如果是根目录就不再往上查找 if path == "/" or path == self.storage_mount_path: print(f"📁 AList 刷新: 根目录不存在,请检查配置") return False # 自动获取父目录并尝试刷新 parent_path = os.path.dirname(path) # 先刷新父目录 parent_result = self.get_file_list(parent_path, True) if parent_result.get("code") == 200: # 再次尝试刷新原目录 retry_data = self.get_file_list(path, True) if retry_data.get("code") == 200: print(f"📁 刷新 AList 目录: [{path}] 成功 ✅") return retry_data.get("data") # 如果刷新父目录后仍不成功,则递归处理父目录 return self.refresh(parent_path, retry_count) elif attempt < retry_count: # 如果还有重试次数,等待后继续 time.sleep(1) # 等待1秒后重试 else: # 已达到最大重试次数 error_msg = data.get("message", "未知错误") print(f"📁 AList 刷新: 失败 ❌ {error_msg}") return None def map_quark_to_alist_path(self, quark_path): """ 将夸克路径映射到AList路径 Args: quark_path: 夸克路径,例如 /Movies/2024 Returns: str: 对应的AList路径,例如 /movies/2024 """ # 确保路径格式正确 if not quark_path.startswith("/"): quark_path = f"/{quark_path}" # 特殊处理根目录的情况 if self.quark_root_dir == "/": # 夸克根目录是/,直接映射到AList挂载路径 if quark_path == "/": return self.storage_mount_path else: # 组合路径 alist_path = os.path.normpath( os.path.join(self.storage_mount_path, quark_path.lstrip("/")) ).replace("\\", "/") return alist_path # 常规情况:检查路径是否在夸克根目录下 if not quark_path.startswith(self.quark_root_dir): # 尝试强制映射,去掉前导路径 relative_path = quark_path.lstrip("/") else: # 去除夸克根目录前缀,并确保路径格式正确 relative_path = quark_path.replace(self.quark_root_dir, "", 1).lstrip("/") # 构建AList路径 alist_path = os.path.normpath( os.path.join(self.storage_mount_path, relative_path) ).replace("\\", "/") return alist_path def get_file_list(self, path, force_refresh=False): """ 获取AList指定路径下的文件列表 Args: path: AList文件路径 force_refresh: 是否强制刷新,默认False Returns: dict: AList API返回的数据 """ url = f"{self.url}/api/fs/list" headers = { "Authorization": self.token, "Content-Type": "application/json", "Accept": "application/json", } payload = { "path": path, "refresh": force_refresh, "password": "", "page": 1, "per_page": 0, } try: response = requests.request( "POST", url, headers=headers, json=payload, timeout=10 ) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"📁 AList 刷新: 网络请求出错 ❌ {str(e)}") except json.JSONDecodeError as e: print(f"📁 AList 刷新: 解析数据出错 ❌ {str(e)}") except Exception as e: print(f"📁 AList 刷新: 未知错误 ❌ {str(e)}") return {"code": 500, "message": "获取文件列表出错"} def get_root_folder_full_path(self, cookie, pdir_fid): """获取夸克根文件夹的完整路径""" if pdir_fid == "0": return "/" url = "https://drive-h.quark.cn/1/clouddrive/file/sort" headers = { "cookie": cookie, "content-type": "application/json", } querystring = { "pr": "ucpro", "fr": "pc", "uc_param_str": "", "pdir_fid": pdir_fid, "_page": 1, "_size": "50", "_fetch_total": "1", "_fetch_sub_dirs": "0", "_sort": "file_type:asc,updated_at:desc", "_fetch_full_path": 1, } try: response = requests.request( "GET", url, headers=headers, params=querystring ).json() if response["code"] == 0: path = "" for item in response["data"]["full_path"]: path = f"{path}/{item['file_name']}" return path except Exception as e: print(f"AList 刷新: 获取路径出错 ❌ {str(e)}") return ""