fix: keep transfer event normalization in domain

This commit is contained in:
jxxghp
2026-05-20 21:03:33 +08:00
parent 5d34bc5c56
commit 0fb9d18b30
4 changed files with 113 additions and 124 deletions
+4
View File
@@ -871,6 +871,8 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
""" """
整理完成后处理 整理完成后处理
""" """
if transferinfo:
transferinfo.ensure_target_items()
# 状态 # 状态
ret_status = True ret_status = True
@@ -1160,6 +1162,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
""" """
if not transferinfo: if not transferinfo:
return None return None
transferinfo.ensure_target_items()
if transferinfo.target_diritem and transferinfo.target_diritem.path: if transferinfo.target_diritem and transferinfo.target_diritem.path:
return transferinfo.target_diritem.path return transferinfo.target_diritem.path
if transferinfo.target_item and transferinfo.target_item.path: if transferinfo.target_item and transferinfo.target_item.path:
@@ -1176,6 +1179,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
""" """
if not transferinfo: if not transferinfo:
return None return None
transferinfo.ensure_target_items()
if transferinfo.target_diritem: if transferinfo.target_diritem:
return transferinfo.target_diritem return transferinfo.target_diritem
target_dir_path = self.__get_transfer_target_dir_path(transferinfo) target_dir_path = self.__get_transfer_target_dir_path(transferinfo)
-104
View File
@@ -6,7 +6,6 @@ import threading
import time import time
import traceback import traceback
import uuid import uuid
from pathlib import Path
from queue import Empty, PriorityQueue from queue import Empty, PriorityQueue
from typing import Callable, Dict, List, Optional, Tuple, Union, Any from typing import Callable, Dict, List, Optional, Tuple, Union, Any
@@ -146,7 +145,6 @@ class EventManager(metaclass=Singleton):
:param priority: 广播事件的优先级,默认为 10 :param priority: 广播事件的优先级,默认为 10
:return: 如果是链式事件,返回处理后的事件数据;否则返回 None :return: 如果是链式事件,返回处理后的事件数据;否则返回 None
""" """
self.__normalize_transfer_event_data(etype, data)
event = Event(etype, data, priority) event = Event(etype, data, priority)
if isinstance(etype, EventType): if isinstance(etype, EventType):
return self.__trigger_broadcast_event(event) return self.__trigger_broadcast_event(event)
@@ -166,7 +164,6 @@ class EventManager(metaclass=Singleton):
:param priority: 广播事件的优先级,默认为 10 :param priority: 广播事件的优先级,默认为 10
:return: 如果是链式事件,返回处理后的事件数据;否则返回 None :return: 如果是链式事件,返回处理后的事件数据;否则返回 None
""" """
self.__normalize_transfer_event_data(etype, data)
event = Event(etype, data, priority) event = Event(etype, data, priority)
if isinstance(etype, EventType): if isinstance(etype, EventType):
return self.__trigger_broadcast_event(event) return self.__trigger_broadcast_event(event)
@@ -176,107 +173,6 @@ class EventManager(metaclass=Singleton):
logger.error(f"Unknown event type: {etype}") logger.error(f"Unknown event type: {etype}")
return None return None
@staticmethod
def __build_transfer_target_item(transferinfo) -> Optional["FileItem"]:
"""
根据目标路径构造整理目标文件项,保证事件消费者能读取 target_item.path。
"""
if transferinfo.target_item and transferinfo.target_item.path:
return transferinfo.target_item
target_path = None
if transferinfo.file_list_new:
target_path = transferinfo.file_list_new[0]
if not target_path:
return transferinfo.target_item
from app.schemas import FileItem
path = Path(str(target_path))
source_item = transferinfo.fileitem
storage = (
transferinfo.target_item.storage
if transferinfo.target_item and transferinfo.target_item.storage
else transferinfo.target_diritem.storage
if transferinfo.target_diritem and transferinfo.target_diritem.storage
else source_item.storage
if source_item and source_item.storage
else "local"
)
return FileItem(
storage=storage,
path=path.as_posix(),
type=source_item.type if source_item and source_item.type else "file",
name=path.name,
basename=path.stem,
extension=path.suffix.lstrip("."),
size=source_item.size if source_item else None,
modify_time=source_item.modify_time if source_item else None,
thumbnail=source_item.thumbnail if source_item else None,
)
@staticmethod
def __build_transfer_target_diritem(transferinfo) -> Optional["FileItem"]:
"""
根据整理结果构造目标目录项,避免事件消费者读取 target_diritem.path 时报错。
"""
if transferinfo.target_diritem and transferinfo.target_diritem.path:
return transferinfo.target_diritem
target_dir_path = None
if transferinfo.target_item and transferinfo.target_item.path:
target_dir_path = Path(str(transferinfo.target_item.path)).parent.as_posix()
elif transferinfo.file_list_new:
target_dir_path = Path(str(transferinfo.file_list_new[0])).parent.as_posix()
if not target_dir_path:
return transferinfo.target_diritem
from app.schemas import FileItem
path = Path(target_dir_path)
storage = (
transferinfo.target_diritem.storage
if transferinfo.target_diritem and transferinfo.target_diritem.storage
else transferinfo.target_item.storage
if transferinfo.target_item and transferinfo.target_item.storage
else transferinfo.fileitem.storage
if transferinfo.fileitem and transferinfo.fileitem.storage
else "local"
)
return FileItem(
storage=storage,
path=path.as_posix(),
type="dir",
name=path.name,
basename=path.stem,
)
@classmethod
def __normalize_transfer_event_data(
cls, etype: Union[EventType, ChainEventType], data: Optional[Union[Dict, ChainEventData]]
) -> None:
"""
整理事件发出前补齐目标文件和目录信息,维持插件侧可直接读取 path 的事件契约。
"""
if not isinstance(etype, EventType) or not isinstance(data, dict):
return
if etype not in {
EventType.TransferComplete,
EventType.TransferFailed,
EventType.SubtitleTransferComplete,
EventType.SubtitleTransferFailed,
EventType.AudioTransferComplete,
EventType.AudioTransferFailed,
EventType.MetadataScrape,
}:
return
transferinfo = data.get("transferinfo")
if not transferinfo or not hasattr(transferinfo, "file_list_new"):
return
transferinfo.target_item = cls.__build_transfer_target_item(transferinfo)
transferinfo.target_diritem = cls.__build_transfer_target_diritem(transferinfo)
def add_event_listener(self, event_type: Union[EventType, ChainEventType], handler: Callable, def add_event_listener(self, event_type: Union[EventType, ChainEventType], handler: Callable,
priority: Optional[int] = DEFAULT_EVENT_PRIORITY): priority: Optional[int] = DEFAULT_EVENT_PRIORITY):
""" """
+81
View File
@@ -134,6 +134,87 @@ class TransferInfo(BaseModel):
# 是否需要通知 # 是否需要通知
need_notify: Optional[bool] = False need_notify: Optional[bool] = False
def ensure_target_items(self):
"""
补齐整理目标文件项和目录项,兼容 OpenList 等成功后元数据延迟可见的存储。
"""
if not self.target_item or not self.target_item.path:
self.target_item = self.__build_target_item()
if not self.target_diritem or not self.target_diritem.path:
self.target_diritem = self.__build_target_diritem()
return self
def __build_target_item(self) -> Optional[FileItem]:
"""
根据目标文件清单构造整理目标文件项。
"""
if self.target_item and self.target_item.path:
return self.target_item
if not self.file_list_new:
return self.target_item
target_path = self.file_list_new[0]
if not target_path:
return self.target_item
path = Path(str(target_path))
source_item = self.fileitem
return FileItem(
storage=self.__get_target_storage(),
path=path.as_posix(),
type=source_item.type if source_item and source_item.type else "file",
name=path.name,
basename=path.stem,
extension=path.suffix.lstrip("."),
size=source_item.size if source_item else None,
modify_time=source_item.modify_time if source_item else None,
thumbnail=source_item.thumbnail if source_item else None,
)
def __build_target_diritem(self) -> Optional[FileItem]:
"""
根据目标文件项或目标文件清单构造整理目标目录项。
"""
if self.target_diritem and self.target_diritem.path:
return self.target_diritem
target_dir_path = self.__get_target_dir_path()
if not target_dir_path:
return self.target_diritem
path = Path(str(target_dir_path))
return FileItem(
storage=self.__get_target_storage(),
path=path.as_posix(),
type="dir",
name=path.name,
basename=path.stem,
)
def __get_target_dir_path(self) -> Optional[str]:
"""
从已知整理结果中推导媒体目标目录路径。
"""
if self.target_item and self.target_item.path:
if self.target_item.type == "dir":
return self.target_item.path
return Path(str(self.target_item.path)).parent.as_posix()
if self.file_list_new:
return Path(str(self.file_list_new[0])).parent.as_posix()
return None
def __get_target_storage(self) -> str:
"""
从目标项、目标目录项或源文件项中推导目标存储标识。
"""
if self.target_item and self.target_item.storage:
return self.target_item.storage
if self.target_diritem and self.target_diritem.storage:
return self.target_diritem.storage
if self.fileitem and self.fileitem.storage:
return self.fileitem.storage
return "local"
def to_dict(self): def to_dict(self):
""" """
返回字典 返回字典
@@ -1,17 +1,13 @@
import unittest import unittest
from unittest.mock import patch
from app.core.event import EventManager
from app.schemas import FileItem, TransferInfo from app.schemas import FileItem, TransferInfo
from app.schemas.types import EventType
class EventTransferNormalizationTest(unittest.TestCase): class TransferInfoTest(unittest.TestCase):
def test_transfer_event_fills_missing_target_items_before_dispatch(self): def test_ensure_target_items_fills_missing_target_items_from_target_path(self):
""" """
整理事件投递给插件前应补齐可读取 path 目标文件和目标目录项 整理结果只有目标路径清单时应补齐目标文件和目录项
""" """
event_manager = EventManager()
transferinfo = TransferInfo( transferinfo = TransferInfo(
success=True, success=True,
fileitem=FileItem( fileitem=FileItem(
@@ -26,12 +22,8 @@ class EventTransferNormalizationTest(unittest.TestCase):
], ],
transfer_type="move", transfer_type="move",
) )
event_data = {"transferinfo": transferinfo}
with patch.object( transferinfo.ensure_target_items()
event_manager, "_EventManager__trigger_broadcast_event"
):
event_manager.send_event(EventType.TransferComplete, event_data)
self.assertIsNotNone(transferinfo.target_item) self.assertIsNotNone(transferinfo.target_item)
self.assertIsNotNone(transferinfo.target_diritem) self.assertIsNotNone(transferinfo.target_diritem)
@@ -46,11 +38,31 @@ class EventTransferNormalizationTest(unittest.TestCase):
self.assertEqual("alist", transferinfo.target_item.storage) self.assertEqual("alist", transferinfo.target_item.storage)
self.assertEqual("alist", transferinfo.target_diritem.storage) self.assertEqual("alist", transferinfo.target_diritem.storage)
def test_transfer_event_fills_missing_target_diritem_from_target_item(self): def test_ensure_target_items_keeps_new_model_initial_state(self):
""" """
目标文件项已存在但目录项缺失时事件数据应补齐 target_diritem 新建整理结果模型不应立即改写目标项避免影响失败记录等非事件流程
"""
transferinfo = TransferInfo(
success=True,
fileitem=FileItem(
storage="alist",
path="/downloads/Test.Show.S01E01.mkv",
type="file",
name="Test.Show.S01E01.mkv",
),
file_list_new=[
"/library/Test Show (2026)/Season 1/Test.Show.S01E01.mkv"
],
transfer_type="move",
)
self.assertIsNone(transferinfo.target_item)
self.assertIsNone(transferinfo.target_diritem)
def test_ensure_target_items_fills_missing_target_diritem_from_target_item(self):
"""
目标文件项已存在但目录项缺失时应从目标文件路径推导目录项
""" """
event_manager = EventManager()
transferinfo = TransferInfo( transferinfo = TransferInfo(
success=True, success=True,
fileitem=FileItem( fileitem=FileItem(
@@ -70,12 +82,8 @@ class EventTransferNormalizationTest(unittest.TestCase):
], ],
transfer_type="move", transfer_type="move",
) )
event_data = {"transferinfo": transferinfo}
with patch.object( transferinfo.ensure_target_items()
event_manager, "_EventManager__trigger_broadcast_event"
):
event_manager.send_event(EventType.TransferComplete, event_data)
self.assertIsNotNone(transferinfo.target_diritem) self.assertIsNotNone(transferinfo.target_diritem)
self.assertEqual( self.assertEqual(