diff --git a/app/modules/filemanager/transhandler.py b/app/modules/filemanager/transhandler.py index 36aeae47..a6a3026e 100644 --- a/app/modules/filemanager/transhandler.py +++ b/app/modules/filemanager/transhandler.py @@ -13,8 +13,14 @@ from app.helper.directory import DirectoryHelper from app.helper.message import TemplateHelper from app.log import logger from app.modules.filemanager.storages import StorageBase -from app.schemas import TransferInfo, TmdbEpisode, TransferDirectoryConf, FileItem, TransferInterceptEventData, \ - TransferRenameEventData +from app.schemas import ( + TransferInfo, + TmdbEpisode, + TransferDirectoryConf, + FileItem, + TransferInterceptEventData, + TransferRenameEventData, +) from app.schemas.types import MediaType, ChainEventType from app.utils.system import SystemUtils @@ -51,26 +57,27 @@ class TransHandler: elif isinstance(current_value, bool): current_value = value elif isinstance(current_value, int): - current_value += (value or 0) + current_value += value or 0 else: current_value = value setattr(result, key, current_value) - def transfer_media(self, - fileitem: FileItem, - in_meta: MetaBase, - mediainfo: MediaInfo, - target_storage: str, - target_path: Path, - transfer_type: str, - source_oper: StorageBase, - target_oper: StorageBase, - need_scrape: Optional[bool] = False, - need_rename: Optional[bool] = True, - need_notify: Optional[bool] = True, - overwrite_mode: Optional[str] = None, - episodes_info: List[TmdbEpisode] = None - ) -> TransferInfo: + def transfer_media( + self, + fileitem: FileItem, + in_meta: MetaBase, + mediainfo: MediaInfo, + target_storage: str, + target_path: Path, + transfer_type: str, + source_oper: StorageBase, + target_oper: StorageBase, + need_scrape: Optional[bool] = False, + need_rename: Optional[bool] = True, + need_notify: Optional[bool] = True, + overwrite_mode: Optional[str] = None, + episodes_info: List[TmdbEpisode] = None, + ) -> TransferInfo: """ 识别并整理一个文件或者一个目录下的所有文件 :param fileitem: 整理的文件对象,可能是一个文件也可以是一个目录 @@ -109,7 +116,9 @@ class TransHandler: """ if not _fileitem.extension: return False - if f".{_fileitem.extension.lower()}" in (settings.RMT_SUBEXT + settings.RMT_AUDIOEXT): + if f".{_fileitem.extension.lower()}" in ( + settings.RMT_SUBEXT + settings.RMT_AUDIOEXT + ): return True return False @@ -117,7 +126,6 @@ class TransHandler: result = TransferInfo() try: - # 重命名格式 rename_format = settings.RENAME_FORMAT(mediainfo.type) @@ -128,9 +136,11 @@ class TransHandler: new_path = self.get_rename_path( path=target_path, template_string=rename_format, - rename_dict=self.get_naming_dict(meta=in_meta, - mediainfo=mediainfo), - source_path=fileitem.path + rename_dict=self.get_naming_dict( + meta=in_meta, mediainfo=mediainfo + ), + source_path=fileitem.path, + source_item=fileitem, ) new_path = DirectoryHelper.get_media_root_path( rename_format, rename_path=new_path @@ -149,40 +159,46 @@ class TransHandler: new_path = target_path / fileitem.name # 原盘大小只计算STREAM目录内的文件大小 if stream_fileitem := source_oper.get_item( - Path(fileitem.path) / "BDMV" / "STREAM" + Path(fileitem.path) / "BDMV" / "STREAM" ): fileitem.size = sum( file.size for file in source_oper.list(stream_fileitem) or [] ) # 整理目录 - new_diritem, errmsg = self.__transfer_dir(fileitem=fileitem, - mediainfo=mediainfo, - source_oper=source_oper, - target_oper=target_oper, - target_storage=target_storage, - target_path=new_path, - transfer_type=transfer_type, - result=result) + new_diritem, errmsg = self.__transfer_dir( + fileitem=fileitem, + mediainfo=mediainfo, + source_oper=source_oper, + target_oper=target_oper, + target_storage=target_storage, + target_path=new_path, + transfer_type=transfer_type, + result=result, + ) if not new_diritem: logger.error(f"文件夹 {fileitem.path} 整理失败:{errmsg}") - self.__update_result(result=result, - success=False, - message=errmsg, - fileitem=fileitem, - transfer_type=transfer_type, - need_notify=need_notify) + self.__update_result( + result=result, + success=False, + message=errmsg, + fileitem=fileitem, + transfer_type=transfer_type, + need_notify=need_notify, + ) return result logger.info(f"文件夹 {fileitem.path} 整理成功") # 返回整理后的路径 - self.__update_result(result=result, - success=True, - fileitem=fileitem, - target_item=new_diritem, - target_diritem=new_diritem, - need_scrape=need_scrape, - need_notify=need_notify, - transfer_type=transfer_type) + self.__update_result( + result=result, + success=True, + fileitem=fileitem, + target_item=new_diritem, + target_diritem=new_diritem, + need_scrape=need_scrape, + need_notify=need_notify, + transfer_type=transfer_type, + ) return result else: # 整理单个文件 @@ -190,13 +206,15 @@ class TransHandler: # 电视剧 if in_meta.begin_episode is None: logger.warn(f"文件 {fileitem.path} 整理失败:未识别到文件集数") - self.__update_result(result=result, - success=False, - message="未识别到文件集数", - fileitem=fileitem, - fail_list=[fileitem.path], - transfer_type=transfer_type, - need_notify=need_notify) + self.__update_result( + result=result, + success=False, + message="未识别到文件集数", + fileitem=fileitem, + fail_list=[fileitem.path], + transfer_type=transfer_type, + need_notify=need_notify, + ) return result # 文件结束季为空 @@ -218,9 +236,10 @@ class TransHandler: meta=in_meta, mediainfo=mediainfo, episodes_info=episodes_info, - file_ext=f".{fileitem.extension}" + file_ext=f".{fileitem.extension}", ), - source_path=fileitem.path + source_path=fileitem.path, + source_item=fileitem, ) # 针对字幕文件,文件名中补充额外标识信息 @@ -250,13 +269,15 @@ class TransHandler: target_diritem = target_oper.get_folder(folder_path) if not target_diritem: logger.error(f"目标目录 {folder_path} 获取失败") - self.__update_result(result=result, - success=False, - message=f"目标目录 {folder_path} 获取失败", - fileitem=fileitem, - fail_list=[fileitem.path], - transfer_type=transfer_type, - need_notify=need_notify) + self.__update_result( + result=result, + success=False, + message=f"目标目录 {folder_path} 获取失败", + fileitem=fileitem, + fail_list=[fileitem.path], + transfer_type=transfer_type, + need_notify=need_notify, + ) return result # 判断是否要覆盖,附加文件强制覆盖 @@ -274,92 +295,112 @@ class TransHandler: if not overflag: # 目标文件已存在 logger.info( - f"目的文件系统中已经存在同名文件 {target_file},当前整理覆盖模式设置为 {overwrite_mode}") - if overwrite_mode == 'always': + f"目的文件系统中已经存在同名文件 {target_file},当前整理覆盖模式设置为 {overwrite_mode}" + ) + if overwrite_mode == "always": # 总是覆盖同名文件 overflag = True - elif overwrite_mode == 'size': + elif overwrite_mode == "size": # 存在时大覆盖小 if target_item.size < fileitem.size: - logger.info(f"目标文件文件大小更小,将覆盖:{new_file}") + logger.info( + f"目标文件文件大小更小,将覆盖:{new_file}" + ) overflag = True else: - self.__update_result(result=result, - success=False, - message=f"媒体库存在同名文件,且质量更好", - fileitem=fileitem, - target_item=target_item, - target_diritem=target_diritem, - fail_list=[fileitem.path], - transfer_type=transfer_type, - need_notify=need_notify) + self.__update_result( + result=result, + success=False, + message=f"媒体库存在同名文件,且质量更好", + fileitem=fileitem, + target_item=target_item, + target_diritem=target_diritem, + fail_list=[fileitem.path], + transfer_type=transfer_type, + need_notify=need_notify, + ) return result - elif overwrite_mode == 'never': + elif overwrite_mode == "never": # 存在不覆盖 - self.__update_result(result=result, - success=False, - message=f"媒体库存在同名文件,当前覆盖模式为不覆盖", - fileitem=fileitem, - target_item=target_item, - target_diritem=target_diritem, - fail_list=[fileitem.path], - transfer_type=transfer_type, - need_notify=need_notify) + self.__update_result( + result=result, + success=False, + message=f"媒体库存在同名文件,当前覆盖模式为不覆盖", + fileitem=fileitem, + target_item=target_item, + target_diritem=target_diritem, + fail_list=[fileitem.path], + transfer_type=transfer_type, + need_notify=need_notify, + ) return result - elif overwrite_mode == 'latest': + elif overwrite_mode == "latest": # 仅保留最新版本 - logger.info(f"当前整理覆盖模式设置为仅保留最新版本,将覆盖:{new_file}") + logger.info( + f"当前整理覆盖模式设置为仅保留最新版本,将覆盖:{new_file}" + ) overflag = True else: - if overwrite_mode == 'latest': + if overwrite_mode == "latest": # 文件不存在,但仅保留最新版本 logger.info( - f"当前整理覆盖模式设置为 {overwrite_mode},仅保留最新版本,正在删除已有版本文件 ...") + f"当前整理覆盖模式设置为 {overwrite_mode},仅保留最新版本,正在删除已有版本文件 ..." + ) self.__delete_version_files(target_oper, new_file) else: # 附加文件 总是需要覆盖 overflag = True # 整理文件 - new_item, err_msg = self.__transfer_file(fileitem=fileitem, - mediainfo=mediainfo, - target_storage=target_storage, - target_file=new_file, - transfer_type=transfer_type, - over_flag=overflag, - source_oper=source_oper, - target_oper=target_oper, - result=result) + new_item, err_msg = self.__transfer_file( + fileitem=fileitem, + mediainfo=mediainfo, + target_storage=target_storage, + target_file=new_file, + transfer_type=transfer_type, + over_flag=overflag, + source_oper=source_oper, + target_oper=target_oper, + result=result, + ) if not new_item: logger.error(f"文件 {fileitem.path} 整理失败:{err_msg}") - self.__update_result(result=result, - success=False, - message=err_msg, - fileitem=fileitem, - fail_list=[fileitem.path], - transfer_type=transfer_type, - need_notify=need_notify) + self.__update_result( + result=result, + success=False, + message=err_msg, + fileitem=fileitem, + fail_list=[fileitem.path], + transfer_type=transfer_type, + need_notify=need_notify, + ) return result logger.info(f"文件 {fileitem.path} 整理成功") - self.__update_result(result=result, - success=True, - fileitem=fileitem, - target_item=new_item, - target_diritem=target_diritem, - need_scrape=need_scrape, - transfer_type=transfer_type, - need_notify=need_notify) + self.__update_result( + result=result, + success=True, + fileitem=fileitem, + target_item=new_item, + target_diritem=target_diritem, + need_scrape=need_scrape, + transfer_type=transfer_type, + need_notify=need_notify, + ) return result except Exception as e: logger.error(f"媒体整理出错:{e}") return TransferInfo(success=False, message=str(e)) @staticmethod - def __transfer_command(fileitem: FileItem, target_storage: str, - source_oper: StorageBase, target_oper: StorageBase, - target_file: Path, transfer_type: str, - ) -> Tuple[Optional[FileItem], str]: + def __transfer_command( + fileitem: FileItem, + target_storage: str, + source_oper: StorageBase, + target_oper: StorageBase, + target_file: Path, + transfer_type: str, + ) -> Tuple[Optional[FileItem], str]: """ 处理单个文件 :param fileitem: 源文件 @@ -381,12 +422,15 @@ class TransHandler: basename=_path.stem, type="file", size=_path.stat().st_size, - extension=_path.suffix.lstrip('.'), - modify_time=_path.stat().st_mtime + extension=_path.suffix.lstrip("."), + modify_time=_path.stat().st_mtime, ) - if (fileitem.storage != target_storage - and fileitem.storage != "local" and target_storage != "local"): + if ( + fileitem.storage != target_storage + and fileitem.storage != "local" + and target_storage != "local" + ): return None, f"不支持 {fileitem.storage} 到 {target_storage} 的文件整理" if fileitem.storage == "local" and target_storage == "local": @@ -419,20 +463,27 @@ class TransHandler: target_fileitem = target_oper.get_folder(target_file.parent) if target_fileitem: # 上传文件 - new_item = target_oper.upload(target_fileitem, filepath, target_file.name) + new_item = target_oper.upload( + target_fileitem, filepath, target_file.name + ) if new_item: return new_item, "" else: return None, f"{fileitem.path} 上传 {target_storage} 失败" else: - return None, f"【{target_storage}】{target_file.parent} 目录获取失败" + return ( + None, + f"【{target_storage}】{target_file.parent} 目录获取失败", + ) elif transfer_type == "move": # 移动 # 根据目的路径获取文件夹 target_fileitem = target_oper.get_folder(target_file.parent) if target_fileitem: # 上传文件 - new_item = target_oper.upload(target_fileitem, filepath, target_file.name) + new_item = target_oper.upload( + target_fileitem, filepath, target_file.name + ) if new_item: # 删除源文件 source_oper.delete(fileitem) @@ -440,7 +491,10 @@ class TransHandler: else: return None, f"{fileitem.path} 上传 {target_storage} 失败" else: - return None, f"【{target_storage}】{target_file.parent} 目录获取失败" + return ( + None, + f"【{target_storage}】{target_file.parent} 目录获取失败", + ) elif fileitem.storage != "local" and target_storage == "local": # 网盘到本地 if target_file.exists(): @@ -449,7 +503,9 @@ class TransHandler: # 网盘到本地 if transfer_type in ["copy", "move"]: # 下载 - tmp_file = source_oper.download(fileitem=fileitem, path=target_file.parent) + tmp_file = source_oper.download( + fileitem=fileitem, path=target_file.parent + ) if tmp_file: # 创建目录 if not target_file.parent.exists(): @@ -471,22 +527,32 @@ class TransHandler: # 复制文件到新目录 target_fileitem = target_oper.get_folder(target_file.parent) if target_fileitem: - if source_oper.copy(fileitem, Path(target_fileitem.path), target_file.name): + if source_oper.copy( + fileitem, Path(target_fileitem.path), target_file.name + ): return target_oper.get_item(target_file), "" else: return None, f"【{target_storage}】{fileitem.path} 复制文件失败" else: - return None, f"【{target_storage}】{target_file.parent} 目录获取失败" + return ( + None, + f"【{target_storage}】{target_file.parent} 目录获取失败", + ) elif transfer_type == "move": # 移动文件到新目录 target_fileitem = target_oper.get_folder(target_file.parent) if target_fileitem: - if source_oper.move(fileitem, Path(target_fileitem.path), target_file.name): + if source_oper.move( + fileitem, Path(target_fileitem.path), target_file.name + ): return target_oper.get_item(target_file), "" else: return None, f"【{target_storage}】{fileitem.path} 移动文件失败" else: - return None, f"【{target_storage}】{target_file.parent} 目录获取失败" + return ( + None, + f"【{target_storage}】{target_file.parent} 目录获取失败", + ) elif transfer_type == "link": if source_oper.link(fileitem, target_file): return target_oper.get_item(target_file), "" @@ -503,22 +569,28 @@ class TransHandler: 重命名字幕文件,补充附加信息 """ # 字幕正则式 - _zhcn_sub_re = r"([.\[(\s](((zh[-_])?(cn|ch[si]|sg|sc))|zho?" \ - r"|chinese|(cn|ch[si]|sg|zho?)[-_&]?(cn|ch[si]|sg|zho?|eng|jap|ja|jpn)" \ - r"|eng[-_&]?(cn|ch[si]|sg|zho?)|(jap|ja|jpn)[-_&]?(cn|ch[si]|sg|zho?)" \ - r"|简[体中]?)[.\])\s])" \ - r"|([\u4e00-\u9fa5]{0,3}[中双][\u4e00-\u9fa5]{0,2}[字文语][\u4e00-\u9fa5]{0,3})" \ - r"|简体|简中|JPSC|sc_jp" \ - r"|(? Tuple[Optional[FileItem], str]: + def __transfer_dir( + self, + fileitem: FileItem, + mediainfo: MediaInfo, + source_oper: StorageBase, + target_oper: StorageBase, + transfer_type: str, + target_storage: str, + target_path: Path, + result: TransferInfo, + ) -> Tuple[Optional[FileItem], str]: """ 整理整个文件夹 :param fileitem: 源文件 @@ -570,7 +651,7 @@ class TransHandler: mediainfo=mediainfo, target_storage=target_storage, target_path=target_path, - transfer_type=transfer_type + transfer_type=transfer_type, ) event = eventmanager.send_event(ChainEventType.TransferIntercept, event_data) if event and event.event_data: @@ -579,25 +660,34 @@ class TransHandler: if event_data.cancel: logger.debug( f"Transfer dir canceled by event: {event_data.source}," - f"Reason: {event_data.reason}") + f"Reason: {event_data.reason}" + ) return None, event_data.reason # 处理所有文件 - state, errmsg = self.__transfer_dir_files(fileitem=fileitem, - target_storage=target_storage, - source_oper=source_oper, - target_oper=target_oper, - target_path=target_path, - transfer_type=transfer_type, - result=result) + state, errmsg = self.__transfer_dir_files( + fileitem=fileitem, + target_storage=target_storage, + source_oper=source_oper, + target_oper=target_oper, + target_path=target_path, + transfer_type=transfer_type, + result=result, + ) if state: return target_item, errmsg else: return None, errmsg - def __transfer_dir_files(self, fileitem: FileItem, target_storage: str, - source_oper: StorageBase, target_oper: StorageBase, - transfer_type: str, target_path: Path, - result: TransferInfo) -> Tuple[bool, str]: + def __transfer_dir_files( + self, + fileitem: FileItem, + target_storage: str, + source_oper: StorageBase, + target_oper: StorageBase, + transfer_type: str, + target_path: Path, + result: TransferInfo, + ) -> Tuple[bool, str]: """ 按目录结构整理目录下所有文件 :param fileitem: 源文件 @@ -613,24 +703,28 @@ class TransHandler: if item.type == "dir": # 递归整理目录 new_path = target_path / item.name - state, errmsg = self.__transfer_dir_files(fileitem=item, - target_storage=target_storage, - source_oper=source_oper, - target_oper=target_oper, - transfer_type=transfer_type, - target_path=new_path, - result=result) + state, errmsg = self.__transfer_dir_files( + fileitem=item, + target_storage=target_storage, + source_oper=source_oper, + target_oper=target_oper, + transfer_type=transfer_type, + target_path=new_path, + result=result, + ) if not state: return False, errmsg else: # 整理文件 new_file = target_path / item.name - new_item, errmsg = self.__transfer_command(fileitem=item, - target_storage=target_storage, - source_oper=source_oper, - target_oper=target_oper, - target_file=new_file, - transfer_type=transfer_type) + new_item, errmsg = self.__transfer_command( + fileitem=item, + target_storage=target_storage, + source_oper=source_oper, + target_oper=target_oper, + target_file=new_file, + transfer_type=transfer_type, + ) if not new_item: return False, errmsg self.__update_result( @@ -641,11 +735,18 @@ class TransHandler: # 返回成功 return True, "" - def __transfer_file(self, fileitem: FileItem, mediainfo: MediaInfo, - source_oper: StorageBase, target_oper: StorageBase, - target_storage: str, target_file: Path, - transfer_type: str, result: TransferInfo, - over_flag: Optional[bool] = False) -> Tuple[Optional[FileItem], str]: + def __transfer_file( + self, + fileitem: FileItem, + mediainfo: MediaInfo, + source_oper: StorageBase, + target_oper: StorageBase, + target_storage: str, + target_file: Path, + transfer_type: str, + result: TransferInfo, + over_flag: Optional[bool] = False, + ) -> Tuple[Optional[FileItem], str]: """ 整理一个文件,同时处理其他相关文件 :param fileitem: 原文件 @@ -659,17 +760,17 @@ class TransHandler: :param source_oper: 源存储操作对象 :param target_oper: 目标存储操作对象 """ - logger.info(f"正在整理文件:【{fileitem.storage}】{fileitem.path} 到 【{target_storage}】{target_file}," - f"操作类型:{transfer_type}") + logger.info( + f"正在整理文件:【{fileitem.storage}】{fileitem.path} 到 【{target_storage}】{target_file}," + f"操作类型:{transfer_type}" + ) event_data = TransferInterceptEventData( fileitem=fileitem, mediainfo=mediainfo, target_storage=target_storage, target_path=target_file, transfer_type=transfer_type, - options={ - "over_flag": over_flag - } + options={"over_flag": over_flag}, ) event = eventmanager.send_event(ChainEventType.TransferIntercept, event_data) if event and event.event_data: @@ -678,9 +779,12 @@ class TransHandler: if event_data.cancel: logger.debug( f"Transfer file canceled by event: {event_data.source}," - f"Reason: {event_data.reason}") + f"Reason: {event_data.reason}" + ) return None, event_data.reason - if target_storage == "local" and (target_file.exists() or target_file.is_symlink()): + if target_storage == "local" and ( + target_file.exists() or target_file.is_symlink() + ): if not over_flag: logger.warn(f"文件已存在:{target_file}") return None, f"{target_file} 已存在" @@ -694,15 +798,19 @@ class TransHandler: logger.warn(f"文件已存在:【{target_storage}】{target_file}") return None, f"【{target_storage}】{target_file} 已存在" else: - logger.info(f"正在删除已存在的文件:【{target_storage}】{target_file}") + logger.info( + f"正在删除已存在的文件:【{target_storage}】{target_file}" + ) target_oper.delete(exists_item) # 执行文件整理命令 - new_item, errmsg = self.__transfer_command(fileitem=fileitem, - target_storage=target_storage, - source_oper=source_oper, - target_oper=target_oper, - target_file=target_file, - transfer_type=transfer_type) + new_item, errmsg = self.__transfer_command( + fileitem=fileitem, + target_storage=target_storage, + source_oper=source_oper, + target_oper=target_oper, + target_file=target_file, + transfer_type=transfer_type, + ) if new_item: self.__update_result( result=result, @@ -716,8 +824,12 @@ class TransHandler: return None, errmsg @staticmethod - def get_dest_path(mediainfo: MediaInfo, target_path: Path, - need_type_folder: Optional[bool] = False, need_category_folder: Optional[bool] = False): + def get_dest_path( + mediainfo: MediaInfo, + target_path: Path, + need_type_folder: Optional[bool] = False, + need_category_folder: Optional[bool] = False, + ): """ 获取目标路径 """ @@ -728,8 +840,12 @@ class TransHandler: return target_path @staticmethod - def get_dest_dir(mediainfo: MediaInfo, target_dir: TransferDirectoryConf, - need_type_folder: Optional[bool] = None, need_category_folder: Optional[bool] = None) -> Path: + def get_dest_dir( + mediainfo: MediaInfo, + target_dir: TransferDirectoryConf, + need_type_folder: Optional[bool] = None, + need_category_folder: Optional[bool] = None, + ) -> Path: """ 根据设置并装媒体库目录 :param mediainfo: 媒体信息 @@ -749,7 +865,11 @@ class TransHandler: library_dir = Path(target_dir.library_path) / target_dir.media_type else: library_dir = Path(target_dir.library_path) - if not target_dir.media_category and need_category_folder and mediainfo.category: + if ( + not target_dir.media_category + and need_category_folder + and mediainfo.category + ): # 二级自动分类 library_dir = library_dir / mediainfo.category elif target_dir.media_category and need_category_folder: @@ -759,8 +879,12 @@ class TransHandler: return library_dir @staticmethod - def get_naming_dict(meta: MetaBase, mediainfo: MediaInfo, file_ext: Optional[str] = None, - episodes_info: List[TmdbEpisode] = None) -> dict: + def get_naming_dict( + meta: MetaBase, + mediainfo: MediaInfo, + file_ext: Optional[str] = None, + episodes_info: List[TmdbEpisode] = None, + ) -> dict: """ 根据媒体信息,返回Format字典 :param meta: 文件元数据 @@ -768,8 +892,12 @@ class TransHandler: :param file_ext: 文件扩展名 :param episodes_info: 当前季的全部集信息 """ - return TemplateHelper().builder.build(meta=meta, mediainfo=mediainfo, - file_extension=file_ext, episodes_info=episodes_info) + return TemplateHelper().builder.build( + meta=meta, + mediainfo=mediainfo, + file_extension=file_ext, + episodes_info=episodes_info, + ) @staticmethod def __delete_version_files(storage_oper: StorageBase, path: Path) -> bool: @@ -816,14 +944,20 @@ class TransHandler: return True @staticmethod - def get_rename_path(template_string: str, rename_dict: dict, - path: Path = None, source_path: str = None) -> Path: + def get_rename_path( + template_string: str, + rename_dict: dict, + path: Optional[Path] = None, + source_path: Optional[str] = None, + source_item: Optional[FileItem] = None, + ) -> Path: """ 生成重命名后的完整路径,支持智能重命名事件 :param template_string: Jinja2 模板字符串 :param rename_dict: 渲染上下文,用于替换模板中的变量 :param path: 可选的基础路径,如果提供,将在其基础上拼接生成的路径 :param source_path: 源文件路径,即待整理的文件路径 + :param source_item: 源文件信息,即待整理的文件信息 :return: 生成的完整路径 """ # 创建jinja2模板对象 @@ -838,15 +972,18 @@ class TransHandler: rename_dict=rename_dict, render_str=render_str, path=path, - source_path=source_path + source_path=source_path, + source_item=source_item, ) event = eventmanager.send_event(ChainEventType.TransferRename, event_data) # 检查事件返回的结果 if event and event.event_data: event_data: TransferRenameEventData = event.event_data if event_data.updated and event_data.updated_str: - logger.debug(f"Render string updated by event: " - f"{render_str} -> {event_data.updated_str} (source: {event_data.source})") + logger.debug( + f"Render string updated by event: " + f"{render_str} -> {event_data.updated_str} (source: {event_data.source})" + ) render_str = event_data.updated_str # 目的路径 diff --git a/app/schemas/event.py b/app/schemas/event.py index 3d6c7db5..86999615 100644 --- a/app/schemas/event.py +++ b/app/schemas/event.py @@ -11,6 +11,7 @@ class Event(BaseModel): """ 事件模型 """ + event_type: str = Field(..., description="事件类型") event_data: Optional[dict] = Field(default={}, description="事件数据") priority: Optional[int] = Field(0, description="事件优先级") @@ -20,6 +21,7 @@ class BaseEventData(BaseModel): """ 事件数据的基类,所有具体事件数据类应继承自此类 """ + pass @@ -27,11 +29,14 @@ class ConfigChangeEventData(BaseEventData): """ ConfigChange 事件的数据模型 """ + key: set[str] = Field(..., description="配置项的键(集合类型)") value: Optional[Any] = Field(default=None, description="配置项的新值") - change_type: str = Field(default="update", description="配置项的变更类型,如 'add', 'update', 'delete'") + change_type: str = Field( + default="update", description="配置项的变更类型,如 'add', 'update', 'delete'" + ) - @field_validator('key', mode='before') + @field_validator("key", mode="before") @classmethod def convert_to_set(cls, v): """将输入的 str、list、dict.keys() 等转为 set""" @@ -55,6 +60,7 @@ class ChainEventData(BaseEventData): """ 链式事件数据的基类,所有具体事件数据类应继承自此类 """ + pass @@ -73,12 +79,24 @@ class AuthCredentials(ChainEventData): channel (Optional[str]): 认证渠道 service (Optional[str]): 服务名称 """ + # 输入参数 - username: Optional[str] = Field(None, description="用户名,适用于 'password' 认证类型") - password: Optional[str] = Field(None, description="用户密码,适用于 'password' 认证类型") - mfa_code: Optional[str] = Field(None, description="一次性密码,目前仅适用于 'password' 认证类型") - code: Optional[str] = Field(None, description="授权码,适用于 'authorization_code' 认证类型") - grant_type: str = Field(..., description="认证类型,如 'password', 'authorization_code', 'client_credentials'") + username: Optional[str] = Field( + None, description="用户名,适用于 'password' 认证类型" + ) + password: Optional[str] = Field( + None, description="用户密码,适用于 'password' 认证类型" + ) + mfa_code: Optional[str] = Field( + None, description="一次性密码,目前仅适用于 'password' 认证类型" + ) + code: Optional[str] = Field( + None, description="授权码,适用于 'authorization_code' 认证类型" + ) + grant_type: str = Field( + ..., + description="认证类型,如 'password', 'authorization_code', 'client_credentials'", + ) # scope: List[str] = Field(default_factory=list, description="权限范围,如 ['read', 'write']") # 输出参数 @@ -87,7 +105,7 @@ class AuthCredentials(ChainEventData): channel: Optional[str] = Field(default=None, description="认证渠道") service: Optional[str] = Field(default=None, description="服务名称") - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def check_fields_based_on_grant_type(cls, values): # noqa grant_type = values.get("grant_type") @@ -97,7 +115,9 @@ class AuthCredentials(ChainEventData): if grant_type == "password": if not values.get("username") or not values.get("password"): - raise ValueError("username and password are required for grant_type 'password'") + raise ValueError( + "username and password are required for grant_type 'password'" + ) elif grant_type == "authorization_code": if not values.get("code"): @@ -122,11 +142,15 @@ class AuthInterceptCredentials(ChainEventData): source (str): 拦截源,默认值为 "未知拦截源" cancel (bool): 是否取消认证,默认值为 False """ + # 输入参数 username: Optional[str] = Field(..., description="用户名") channel: str = Field(..., description="认证渠道") service: str = Field(..., description="服务名称") - status: str = Field(..., description="认证状态, 包含 'triggered' 表示认证触发,'completed' 表示认证成功") + status: str = Field( + ..., + description="认证状态, 包含 'triggered' 表示认证触发,'completed' 表示认证成功", + ) token: Optional[str] = Field(default=None, description="认证令牌") # 输出参数 @@ -148,6 +172,7 @@ class CommandRegisterEventData(ChainEventData): source (str): 拦截源,默认值为 "未知拦截源" cancel (bool): 是否取消认证,默认值为 False """ + # 输入参数 commands: Dict[str, dict] = Field(..., description="菜单命令") origin: str = Field(..., description="事件源") @@ -169,18 +194,25 @@ class TransferRenameEventData(ChainEventData): render_str (str): 渲染生成的字符串 path (Optional[Path]): 当前文件的目标路径 source_path (Optional[str]): 源文件路径,即待整理的文件路径 + source_item (Optional[FileItem]): 源文件信息,即待整理的文件信息 # 输出参数 updated (bool): 是否已更新,默认值为 False updated_str (str): 更新后的字符串 source (str): 拦截源,默认值为 "未知拦截源" """ + # 输入参数 template_string: str = Field(..., description="模板字符串") rename_dict: Dict[str, Any] = Field(..., description="渲染上下文") path: Optional[Path] = Field(None, description="文件的目标路径") render_str: str = Field(..., description="渲染生成的字符串") - source_path: Optional[str] = Field(None, description="源文件路径,即待整理的文件路径") + source_path: Optional[str] = Field( + None, description="源文件路径,即待整理的文件路径" + ) + source_item: Optional[FileItem] = Field( + None, description="源文件信息,即待整理的文件信息" + ) # 输出参数 updated: bool = Field(default=False, description="是否已更新") @@ -202,6 +234,7 @@ class ResourceSelectionEventData(BaseModel): updated_contexts (Optional[List[Context]]): 已更新的资源上下文列表,默认值为 None source (str): 更新源,默认值为 "未知更新源" """ + # 输入参数 contexts: Any = Field(None, description="待选择的资源上下文列表") downloader: Optional[str] = Field(None, description="下载器") @@ -209,7 +242,9 @@ class ResourceSelectionEventData(BaseModel): # 输出参数 updated: bool = Field(default=False, description="是否已更新") - updated_contexts: Optional[List[Any]] = Field(default=None, description="已更新的资源上下文列表") + updated_contexts: Optional[List[Any]] = Field( + default=None, description="已更新的资源上下文列表" + ) source: Optional[str] = Field(default="未知拦截源", description="拦截源") @@ -231,6 +266,7 @@ class ResourceDownloadEventData(ChainEventData): source (str): 拦截源,默认值为 "未知拦截源" reason (str): 拦截原因,描述拦截的具体原因 """ + # 输入参数 context: Any = Field(None, description="当前资源上下文") episodes: Optional[Set[int]] = Field(None, description="需要下载的集数") @@ -262,6 +298,7 @@ class TransferInterceptEventData(ChainEventData): source (str): 拦截源,默认值为 "未知拦截源" reason (str): 拦截原因,描述拦截的具体原因 """ + # 输入参数 fileitem: FileItem = Field(..., description="源文件") mediainfo: Any = Field(..., description="媒体信息") @@ -280,12 +317,17 @@ class DiscoverMediaSource(BaseModel): """ 探索媒体数据源的基类 """ + name: str = Field(..., description="数据源名称") mediaid_prefix: str = Field(..., description="媒体ID的前缀,不含:") api_path: str = Field(..., description="媒体数据源API地址") - filter_params: Optional[Dict[str, Any]] = Field(default=None, description="过滤参数") + filter_params: Optional[Dict[str, Any]] = Field( + default=None, description="过滤参数" + ) filter_ui: Optional[List[dict]] = Field(default=[], description="过滤参数UI配置") - depends: Optional[Dict[str, list]] = Field(default=None, description="UI依赖关系字典") + depends: Optional[Dict[str, list]] = Field( + default=None, description="UI依赖关系字典" + ) class DiscoverSourceEventData(ChainEventData): @@ -296,14 +338,18 @@ class DiscoverSourceEventData(ChainEventData): # 输出参数 extra_sources (List[DiscoverMediaSource]): 额外媒体数据源 """ + # 输出参数 - extra_sources: List[DiscoverMediaSource] = Field(default_factory=list, description="额外媒体数据源") + extra_sources: List[DiscoverMediaSource] = Field( + default_factory=list, description="额外媒体数据源" + ) class RecommendMediaSource(BaseModel): """ 推荐媒体数据源的基类 """ + name: str = Field(..., description="数据源名称") api_path: str = Field(..., description="媒体数据源API地址") type: str = Field(..., description="类型") @@ -317,8 +363,11 @@ class RecommendSourceEventData(ChainEventData): # 输出参数 extra_sources (List[RecommendMediaSource]): 额外媒体数据源 """ + # 输出参数 - extra_sources: List[RecommendMediaSource] = Field(default_factory=list, description="额外媒体数据源") + extra_sources: List[RecommendMediaSource] = Field( + default_factory=list, description="额外媒体数据源" + ) class MediaRecognizeConvertEventData(ChainEventData): @@ -333,12 +382,15 @@ class MediaRecognizeConvertEventData(ChainEventData): # 输出参数 media_dict (dict): TheMovieDb/豆瓣的媒体数据 """ + # 输入参数 mediaid: str = Field(..., description="媒体ID") convert_type: str = Field(..., description="转换类型(themoviedb/douban)") # 输出参数 - media_dict: dict = Field(default_factory=dict, description="转换后的媒体信息(TheMovieDb/豆瓣)") + media_dict: dict = Field( + default_factory=dict, description="转换后的媒体信息(TheMovieDb/豆瓣)" + ) class StorageOperSelectionEventData(ChainEventData): @@ -352,6 +404,7 @@ class StorageOperSelectionEventData(ChainEventData): # 输出参数 storage_oper (Callable): 存储操作对象 """ + # 输入参数 storage: Optional[str] = Field(default=None, description="存储类型")