From 0fb9d18b30ed153ff117d5e393743e776af7055b Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 20 May 2026 21:03:33 +0800 Subject: [PATCH] fix: keep transfer event normalization in domain --- app/chain/transfer.py | 4 + app/core/event.py | 104 ------------------ app/schemas/transfer.py | 81 ++++++++++++++ ...normalization.py => test_transfer_info.py} | 48 ++++---- 4 files changed, 113 insertions(+), 124 deletions(-) rename tests/{test_event_transfer_normalization.py => test_transfer_info.py} (63%) diff --git a/app/chain/transfer.py b/app/chain/transfer.py index 60c9e3a7..82f1dbc6 100755 --- a/app/chain/transfer.py +++ b/app/chain/transfer.py @@ -871,6 +871,8 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): """ 整理完成后处理 """ + if transferinfo: + transferinfo.ensure_target_items() # 状态 ret_status = True @@ -1160,6 +1162,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): """ if not transferinfo: return None + transferinfo.ensure_target_items() if transferinfo.target_diritem and transferinfo.target_diritem.path: return transferinfo.target_diritem.path if transferinfo.target_item and transferinfo.target_item.path: @@ -1176,6 +1179,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): """ if not transferinfo: return None + transferinfo.ensure_target_items() if transferinfo.target_diritem: return transferinfo.target_diritem target_dir_path = self.__get_transfer_target_dir_path(transferinfo) diff --git a/app/core/event.py b/app/core/event.py index a07081c6..c78b5a5a 100644 --- a/app/core/event.py +++ b/app/core/event.py @@ -6,7 +6,6 @@ import threading import time import traceback import uuid -from pathlib import Path from queue import Empty, PriorityQueue from typing import Callable, Dict, List, Optional, Tuple, Union, Any @@ -146,7 +145,6 @@ class EventManager(metaclass=Singleton): :param priority: 广播事件的优先级,默认为 10 :return: 如果是链式事件,返回处理后的事件数据;否则返回 None """ - self.__normalize_transfer_event_data(etype, data) event = Event(etype, data, priority) if isinstance(etype, EventType): return self.__trigger_broadcast_event(event) @@ -166,7 +164,6 @@ class EventManager(metaclass=Singleton): :param priority: 广播事件的优先级,默认为 10 :return: 如果是链式事件,返回处理后的事件数据;否则返回 None """ - self.__normalize_transfer_event_data(etype, data) event = Event(etype, data, priority) if isinstance(etype, EventType): return self.__trigger_broadcast_event(event) @@ -176,107 +173,6 @@ class EventManager(metaclass=Singleton): logger.error(f"Unknown event type: {etype}") 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, priority: Optional[int] = DEFAULT_EVENT_PRIORITY): """ diff --git a/app/schemas/transfer.py b/app/schemas/transfer.py index 0fb2fd73..90e8813e 100644 --- a/app/schemas/transfer.py +++ b/app/schemas/transfer.py @@ -134,6 +134,87 @@ class TransferInfo(BaseModel): # 是否需要通知 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): """ 返回字典 diff --git a/tests/test_event_transfer_normalization.py b/tests/test_transfer_info.py similarity index 63% rename from tests/test_event_transfer_normalization.py rename to tests/test_transfer_info.py index 51152e9d..3e99f24d 100644 --- a/tests/test_event_transfer_normalization.py +++ b/tests/test_transfer_info.py @@ -1,17 +1,13 @@ import unittest -from unittest.mock import patch -from app.core.event import EventManager from app.schemas import FileItem, TransferInfo -from app.schemas.types import EventType -class EventTransferNormalizationTest(unittest.TestCase): - def test_transfer_event_fills_missing_target_items_before_dispatch(self): +class TransferInfoTest(unittest.TestCase): + def test_ensure_target_items_fills_missing_target_items_from_target_path(self): """ - 整理事件投递给插件前,应补齐可读取 path 的目标文件和目标目录项。 + 整理结果只有目标路径清单时,应补齐目标文件项和目录项。 """ - event_manager = EventManager() transferinfo = TransferInfo( success=True, fileitem=FileItem( @@ -26,12 +22,8 @@ class EventTransferNormalizationTest(unittest.TestCase): ], transfer_type="move", ) - event_data = {"transferinfo": transferinfo} - with patch.object( - event_manager, "_EventManager__trigger_broadcast_event" - ): - event_manager.send_event(EventType.TransferComplete, event_data) + transferinfo.ensure_target_items() self.assertIsNotNone(transferinfo.target_item) 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_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( success=True, fileitem=FileItem( @@ -70,12 +82,8 @@ class EventTransferNormalizationTest(unittest.TestCase): ], transfer_type="move", ) - event_data = {"transferinfo": transferinfo} - with patch.object( - event_manager, "_EventManager__trigger_broadcast_event" - ): - event_manager.send_event(EventType.TransferComplete, event_data) + transferinfo.ensure_target_items() self.assertIsNotNone(transferinfo.target_diritem) self.assertEqual(