feat(transfer): 新增 TransferOverwriteCheck 事件支持插件介入覆盖判断

允许插件在覆盖模式判断前提供目标文件的真实大小或直接给出覆盖决策,
解决 .strm 等本地大小不准的场景下 size 模式失效的问题。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DDSRem
2026-04-08 15:19:20 +08:00
committed by jxxghp
parent 3c7cd2186f
commit 4d9f17b083
3 changed files with 103 additions and 2 deletions

View File

@@ -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}"
)

View File

@@ -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):
"""
探索媒体数据源的基类

View File

@@ -156,6 +156,8 @@ class ChainEventType(Enum):
TransferRename = "transfer.rename"
# 整理拦截
TransferIntercept = "transfer.intercept"
# 整理覆盖检查
TransferOverwriteCheck = "transfer.overwrite.check"
# 资源选择
ResourceSelection = "resource.selection"
# 资源下载