From 4d9f17b083a29f00ce1812ba2d10b5f55c67ea0f Mon Sep 17 00:00:00 2001 From: DDSRem <1448139087@qq.com> Date: Wed, 8 Apr 2026 15:19:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(transfer):=20=E6=96=B0=E5=A2=9E=20Transfer?= =?UTF-8?q?OverwriteCheck=20=E4=BA=8B=E4=BB=B6=E6=94=AF=E6=8C=81=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E4=BB=8B=E5=85=A5=E8=A6=86=E7=9B=96=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 允许插件在覆盖模式判断前提供目标文件的真实大小或直接给出覆盖决策, 解决 .strm 等本地大小不准的场景下 size 模式失效的问题。 Co-Authored-By: Claude Opus 4.6 --- app/modules/filemanager/transhandler.py | 56 ++++++++++++++++++++++++- app/schemas/event.py | 47 +++++++++++++++++++++ app/schemas/types.py | 2 + 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/app/modules/filemanager/transhandler.py b/app/modules/filemanager/transhandler.py index a6a3026e..12fe45c4 100644 --- a/app/modules/filemanager/transhandler.py +++ b/app/modules/filemanager/transhandler.py @@ -19,6 +19,7 @@ from app.schemas import ( TransferDirectoryConf, FileItem, TransferInterceptEventData, + TransferOverwriteCheckEventData, TransferRenameEventData, ) from app.schemas.types import MediaType, ChainEventType @@ -297,12 +298,63 @@ class TransHandler: logger.info( f"目的文件系统中已经存在同名文件 {target_file},当前整理覆盖模式设置为 {overwrite_mode}" ) - if overwrite_mode == "always": + # 触发覆盖检查事件,允许插件提供目标文件真实大小 + # 或直接给出覆盖决策(例如 .strm 文件指向网盘原始文件) + overwrite_event_data = TransferOverwriteCheckEventData( + fileitem=fileitem, + target_item=target_item, + target_storage=target_storage, + target_path=new_file, + overwrite_mode=overwrite_mode or "", + transfer_type=transfer_type, + ) + overwrite_event = eventmanager.send_event( + ChainEventType.TransferOverwriteCheck, + overwrite_event_data, + ) + plugin_overwrite: Optional[bool] = None + plugin_target_size: Optional[int] = None + if overwrite_event and overwrite_event.event_data: + overwrite_event_data = overwrite_event.event_data + plugin_overwrite = overwrite_event_data.overwrite + plugin_target_size = overwrite_event_data.target_size + if ( + plugin_overwrite is not None + or plugin_target_size is not None + ): + logger.info( + f"覆盖检查事件由 {overwrite_event_data.source} 处理:" + f"overwrite={plugin_overwrite}, " + f"target_size={plugin_target_size}, " + f"reason={overwrite_event_data.reason}" + ) + if plugin_overwrite is True: + overflag = True + elif plugin_overwrite is False: + self.__update_result( + result=result, + success=False, + message=overwrite_event_data.reason + or "插件决定不覆盖已有文件", + 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 == "always": # 总是覆盖同名文件 overflag = True elif overwrite_mode == "size": # 存在时大覆盖小 - if target_item.size < fileitem.size: + target_size = ( + plugin_target_size + if plugin_target_size is not None + else target_item.size + ) + if target_size < fileitem.size: logger.info( f"目标文件文件大小更小,将覆盖:{new_file}" ) diff --git a/app/schemas/event.py b/app/schemas/event.py index 86999615..0e52eedb 100644 --- a/app/schemas/event.py +++ b/app/schemas/event.py @@ -313,6 +313,53 @@ class TransferInterceptEventData(ChainEventData): reason: str = Field(default="", description="拦截原因") +class TransferOverwriteCheckEventData(ChainEventData): + """ + TransferOverwriteCheck 事件的数据模型 + + 在覆盖模式判断(如按文件大小覆盖)执行之前触发,允许插件提供目标文件 + 的真实大小(例如本地 .strm 文件指向的网盘原始文件大小),或者直接给出 + 覆盖决策。 + + Attributes: + # 输入参数 + fileitem (FileItem): 源文件 + target_item (FileItem): 目标文件(已存在) + target_storage (str): 目标存储 + target_path (Path): 目标文件路径 + overwrite_mode (str): 覆盖模式(always、size、never、latest) + transfer_type (str): 整理方式 + options (dict): 其他参数 + + # 输出参数 + target_size (Optional[int]): 由插件提供的目标文件真实大小,覆盖 + target_item.size 用于 size 模式比较;为 None 时表示不修改 + overwrite (Optional[bool]): 由插件直接给出的覆盖决策,非 None 时 + 将完全跳过 MoviePilot 内置的 size/never/latest 等比较逻辑 + source (str): 处理来源 + reason (str): 处理原因,描述插件做出决策或修改的原因 + """ + + # 输入参数 + fileitem: FileItem = Field(..., description="源文件") + target_item: FileItem = Field(..., description="目标已存在文件") + target_storage: str = Field(..., description="目标存储") + target_path: Path = Field(..., description="目标文件路径") + overwrite_mode: str = Field(..., description="覆盖模式") + transfer_type: str = Field(..., description="整理方式") + options: Optional[dict] = Field(default=None, description="其他参数") + + # 输出参数 + target_size: Optional[int] = Field( + default=None, description="插件提供的目标文件真实大小" + ) + overwrite: Optional[bool] = Field( + default=None, description="插件直接给出的覆盖决策" + ) + source: str = Field(default="未知处理源", description="处理来源") + reason: str = Field(default="", description="处理原因") + + class DiscoverMediaSource(BaseModel): """ 探索媒体数据源的基类 diff --git a/app/schemas/types.py b/app/schemas/types.py index 44e676ec..c3bd82be 100644 --- a/app/schemas/types.py +++ b/app/schemas/types.py @@ -156,6 +156,8 @@ class ChainEventType(Enum): TransferRename = "transfer.rename" # 整理拦截 TransferIntercept = "transfer.intercept" + # 整理覆盖检查 + TransferOverwriteCheck = "transfer.overwrite.check" # 资源选择 ResourceSelection = "resource.selection" # 资源下载