feat: TransferRename 事件增加 source_item 源文件信息

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