mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-01 13:40:54 +08:00
fix: keep transfer event normalization in domain
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
返回字典
|
||||
|
||||
@@ -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(
|
||||
Reference in New Issue
Block a user