mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 08:57:09 +08:00
fix: build complete transfer result at source
This commit is contained in:
+2
-30
@@ -871,9 +871,6 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
整理完成后处理
|
整理完成后处理
|
||||||
"""
|
"""
|
||||||
if transferinfo:
|
|
||||||
transferinfo.ensure_target_items()
|
|
||||||
|
|
||||||
# 状态
|
# 状态
|
||||||
ret_status = True
|
ret_status = True
|
||||||
# 错误信息
|
# 错误信息
|
||||||
@@ -1162,7 +1159,6 @@ 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:
|
||||||
@@ -1171,30 +1167,6 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
return Path(transferinfo.file_list_new[0]).parent.as_posix()
|
return Path(transferinfo.file_list_new[0]).parent.as_posix()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __build_transfer_target_diritem(
|
|
||||||
self, transferinfo: Optional[TransferInfo]
|
|
||||||
) -> Optional[FileItem]:
|
|
||||||
"""
|
|
||||||
构建整理目标目录项,避免成功结果缺少 target_diritem 时阻断后续流程。
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
if not target_dir_path:
|
|
||||||
return None
|
|
||||||
target_path = Path(target_dir_path)
|
|
||||||
storage = transferinfo.target_item.storage if transferinfo.target_item else "local"
|
|
||||||
return FileItem(
|
|
||||||
storage=storage,
|
|
||||||
path=target_dir_path,
|
|
||||||
type="dir",
|
|
||||||
name=target_path.name,
|
|
||||||
basename=target_path.stem,
|
|
||||||
)
|
|
||||||
|
|
||||||
def put_to_queue(self, task: TransferTask) -> bool:
|
def put_to_queue(self, task: TransferTask) -> bool:
|
||||||
"""
|
"""
|
||||||
添加到待整理队列
|
添加到待整理队列
|
||||||
@@ -1248,7 +1220,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
target_diritem = self.__build_transfer_target_diritem(transferinfo)
|
target_diritem = transferinfo.target_diritem
|
||||||
if not target_diritem:
|
if not target_diritem:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1311,7 +1283,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
target_diritem = self.__build_transfer_target_diritem(transferinfo)
|
target_diritem = transferinfo.target_diritem
|
||||||
if not target_diritem:
|
if not target_diritem:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,41 @@ class TransHandler:
|
|||||||
size=size if item_type == "file" else None,
|
size=size if item_type == "file" else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __build_transfer_target_diritem(
|
||||||
|
target_storage: str, target_path: Path
|
||||||
|
) -> FileItem:
|
||||||
|
"""
|
||||||
|
按已确认的目标目录路径构造整理结果目录项。
|
||||||
|
"""
|
||||||
|
return FileItem(
|
||||||
|
storage=target_storage,
|
||||||
|
path=TransHandler.__format_dir_path(target_path),
|
||||||
|
name=target_path.name,
|
||||||
|
basename=target_path.stem,
|
||||||
|
type="dir",
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __format_dir_path(path: Path) -> str:
|
||||||
|
"""
|
||||||
|
按 FileItem 目录路径约定返回以斜杠结尾的路径。
|
||||||
|
"""
|
||||||
|
path_str = path.as_posix()
|
||||||
|
if path_str != "/" and not path_str.endswith("/"):
|
||||||
|
return f"{path_str}/"
|
||||||
|
return path_str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __normalize_dir_path(path: Optional[str]) -> str:
|
||||||
|
"""
|
||||||
|
比较目录路径时忽略尾部斜杠,兼容不同存储返回的目录项格式。
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
return ""
|
||||||
|
path_str = Path(str(path)).as_posix().rstrip("/")
|
||||||
|
return path_str or "/"
|
||||||
|
|
||||||
def transfer_media(
|
def transfer_media(
|
||||||
self,
|
self,
|
||||||
fileitem: FileItem,
|
fileitem: FileItem,
|
||||||
@@ -281,6 +316,10 @@ class TransHandler:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
logger.info(f"文件夹 {fileitem.path} 整理成功")
|
logger.info(f"文件夹 {fileitem.path} 整理成功")
|
||||||
|
if self.__normalize_dir_path(new_diritem.path) != self.__normalize_dir_path(new_path.as_posix()):
|
||||||
|
new_diritem = self.__build_transfer_target_diritem(
|
||||||
|
target_storage=target_storage, target_path=new_path
|
||||||
|
)
|
||||||
# 返回整理后的路径
|
# 返回整理后的路径
|
||||||
self.__update_result(
|
self.__update_result(
|
||||||
result=result,
|
result=result,
|
||||||
@@ -401,6 +440,11 @@ class TransHandler:
|
|||||||
need_notify=need_notify,
|
need_notify=need_notify,
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
# 目标目录已创建成功但元数据可能短暂缺字段,整理结果直接按目标路径补全。
|
||||||
|
if self.__normalize_dir_path(target_diritem.path) != self.__normalize_dir_path(folder_path.as_posix()):
|
||||||
|
target_diritem = self.__build_transfer_target_diritem(
|
||||||
|
target_storage=target_storage, target_path=folder_path
|
||||||
|
)
|
||||||
|
|
||||||
# 判断是否要覆盖,附加文件强制覆盖
|
# 判断是否要覆盖,附加文件强制覆盖
|
||||||
overflag = False
|
overflag = False
|
||||||
|
|||||||
@@ -134,87 +134,6 @@ 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,97 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
from app.schemas import FileItem, TransferInfo
|
|
||||||
|
|
||||||
|
|
||||||
class TransferInfoTest(unittest.TestCase):
|
|
||||||
def test_ensure_target_items_fills_missing_target_items_from_target_path(self):
|
|
||||||
"""
|
|
||||||
整理结果只有目标路径清单时,应补齐目标文件项和目录项。
|
|
||||||
"""
|
|
||||||
transferinfo = TransferInfo(
|
|
||||||
success=True,
|
|
||||||
fileitem=FileItem(
|
|
||||||
storage="alist",
|
|
||||||
path="/downloads/Test.Show.S01E01.mkv",
|
|
||||||
type="file",
|
|
||||||
name="Test.Show.S01E01.mkv",
|
|
||||||
size=1024,
|
|
||||||
),
|
|
||||||
file_list_new=[
|
|
||||||
"/library/Test Show (2026)/Season 1/Test.Show.S01E01.mkv"
|
|
||||||
],
|
|
||||||
transfer_type="move",
|
|
||||||
)
|
|
||||||
|
|
||||||
transferinfo.ensure_target_items()
|
|
||||||
|
|
||||||
self.assertIsNotNone(transferinfo.target_item)
|
|
||||||
self.assertIsNotNone(transferinfo.target_diritem)
|
|
||||||
self.assertEqual(
|
|
||||||
"/library/Test Show (2026)/Season 1/Test.Show.S01E01.mkv",
|
|
||||||
transferinfo.target_item.path,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
"/library/Test Show (2026)/Season 1",
|
|
||||||
transferinfo.target_diritem.path,
|
|
||||||
)
|
|
||||||
self.assertEqual("alist", transferinfo.target_item.storage)
|
|
||||||
self.assertEqual("alist", transferinfo.target_diritem.storage)
|
|
||||||
|
|
||||||
def test_ensure_target_items_keeps_new_model_initial_state(self):
|
|
||||||
"""
|
|
||||||
新建整理结果模型不应立即改写目标项,避免影响失败记录等非事件流程。
|
|
||||||
"""
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
目标文件项已存在但目录项缺失时,应从目标文件路径推导目录项。
|
|
||||||
"""
|
|
||||||
transferinfo = TransferInfo(
|
|
||||||
success=True,
|
|
||||||
fileitem=FileItem(
|
|
||||||
storage="alist",
|
|
||||||
path="/downloads/Test.Show.S01E02.mkv",
|
|
||||||
type="file",
|
|
||||||
name="Test.Show.S01E02.mkv",
|
|
||||||
),
|
|
||||||
target_item=FileItem(
|
|
||||||
storage="alist",
|
|
||||||
path="/library/Test Show (2026)/Season 1/Test.Show.S01E02.mkv",
|
|
||||||
type="file",
|
|
||||||
name="Test.Show.S01E02.mkv",
|
|
||||||
),
|
|
||||||
file_list_new=[
|
|
||||||
"/library/Test Show (2026)/Season 1/Test.Show.S01E02.mkv"
|
|
||||||
],
|
|
||||||
transfer_type="move",
|
|
||||||
)
|
|
||||||
|
|
||||||
transferinfo.ensure_target_items()
|
|
||||||
|
|
||||||
self.assertIsNotNone(transferinfo.target_diritem)
|
|
||||||
self.assertEqual(
|
|
||||||
"/library/Test Show (2026)/Season 1",
|
|
||||||
transferinfo.target_diritem.path,
|
|
||||||
)
|
|
||||||
self.assertEqual("alist", transferinfo.target_diritem.storage)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -4,6 +4,8 @@ from types import SimpleNamespace
|
|||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
from app.core.context import MediaInfo
|
||||||
|
from app.core.meta import MetaVideo
|
||||||
from app.chain.transfer import JobManager, TransferChain
|
from app.chain.transfer import JobManager, TransferChain
|
||||||
from app.modules.filemanager.transhandler import TransHandler
|
from app.modules.filemanager.transhandler import TransHandler
|
||||||
from app.schemas import EpisodeFormat, FileItem, TransferInfo, TransferTask
|
from app.schemas import EpisodeFormat, FileItem, TransferInfo, TransferTask
|
||||||
@@ -73,6 +75,20 @@ class FakeMedia:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_media_info() -> MediaInfo:
|
||||||
|
media = MediaInfo()
|
||||||
|
media.type = MediaType.TV
|
||||||
|
media.title = "Test Show"
|
||||||
|
media.title_year = "Test Show (2026)"
|
||||||
|
media.year = "2026"
|
||||||
|
media.tmdb_id = 12345
|
||||||
|
media.category = ""
|
||||||
|
media.actors = []
|
||||||
|
media.season_years = {}
|
||||||
|
media.vote_average = 0
|
||||||
|
return media
|
||||||
|
|
||||||
|
|
||||||
def make_task(episode: int, season: int = 1) -> TransferTask:
|
def make_task(episode: int, season: int = 1) -> TransferTask:
|
||||||
name = f"Test.Show.S{season:02d}E{episode:02d}.mkv"
|
name = f"Test.Show.S{season:02d}E{episode:02d}.mkv"
|
||||||
return TransferTask(
|
return TransferTask(
|
||||||
@@ -176,6 +192,134 @@ class TransferJobManagerTest(unittest.TestCase):
|
|||||||
self.assertEqual("file", new_item.type)
|
self.assertEqual("file", new_item.type)
|
||||||
self.assertEqual(1024, new_item.size)
|
self.assertEqual(1024, new_item.size)
|
||||||
|
|
||||||
|
def test_transfer_media_passes_complete_result_when_target_metadata_is_delayed(self):
|
||||||
|
"""
|
||||||
|
整理成功时应在结果源头带上完整目标项,回调和事件不再二次拼装。
|
||||||
|
"""
|
||||||
|
handler = TransHandler()
|
||||||
|
source_item = FileItem(
|
||||||
|
storage="alist",
|
||||||
|
path="/downloads/Test.Show.S01E01.mkv",
|
||||||
|
type="file",
|
||||||
|
name="Test.Show.S01E01.mkv",
|
||||||
|
basename="Test.Show.S01E01",
|
||||||
|
extension="mkv",
|
||||||
|
size=1024,
|
||||||
|
modify_time=1715939275.0,
|
||||||
|
)
|
||||||
|
target_path = Path("/library")
|
||||||
|
target_file = Path(
|
||||||
|
"/library/Test.Show.S01E01.mkv"
|
||||||
|
)
|
||||||
|
target_folder = FileItem(
|
||||||
|
storage="alist",
|
||||||
|
type="dir",
|
||||||
|
)
|
||||||
|
target_item = FileItem(
|
||||||
|
storage="alist",
|
||||||
|
path=target_file.as_posix(),
|
||||||
|
type="file",
|
||||||
|
name=target_file.name,
|
||||||
|
basename=target_file.stem,
|
||||||
|
extension="mkv",
|
||||||
|
size=1024,
|
||||||
|
)
|
||||||
|
source_oper = SimpleNamespace(
|
||||||
|
is_support_transtype=lambda transfer_type: True,
|
||||||
|
move=lambda fileitem, path, name: True,
|
||||||
|
)
|
||||||
|
target_oper = SimpleNamespace(
|
||||||
|
get_folder=lambda path: target_folder,
|
||||||
|
get_item=lambda path: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
TransHandler, "get_rename_path", return_value=target_file
|
||||||
|
), patch(
|
||||||
|
"app.modules.filemanager.transhandler.DirectoryHelper.get_media_root_path",
|
||||||
|
return_value=Path("/library"),
|
||||||
|
), patch.object(
|
||||||
|
TransHandler,
|
||||||
|
"_TransHandler__transfer_command",
|
||||||
|
return_value=(target_item, ""),
|
||||||
|
), patch("app.modules.filemanager.transhandler.eventmanager") as eventmanager_mock:
|
||||||
|
eventmanager_mock.send_event.return_value = None
|
||||||
|
transferinfo = handler.transfer_media(
|
||||||
|
fileitem=source_item,
|
||||||
|
in_meta=MetaVideo("Test.Show.S01E01"),
|
||||||
|
mediainfo=make_media_info(),
|
||||||
|
target_storage="alist",
|
||||||
|
target_path=target_path,
|
||||||
|
transfer_type="move",
|
||||||
|
source_oper=source_oper,
|
||||||
|
target_oper=target_oper,
|
||||||
|
need_scrape=True,
|
||||||
|
need_notify=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(transferinfo.success)
|
||||||
|
self.assertEqual(target_item, transferinfo.target_item)
|
||||||
|
self.assertIsNotNone(transferinfo.target_diritem)
|
||||||
|
self.assertEqual("/library/", transferinfo.target_diritem.path)
|
||||||
|
self.assertEqual("alist", transferinfo.target_diritem.storage)
|
||||||
|
self.assertEqual("dir", transferinfo.target_diritem.type)
|
||||||
|
|
||||||
|
def test_success_callback_uses_transfer_result_target_diritem(self):
|
||||||
|
"""
|
||||||
|
回调发送刮削事件时应直接使用整理结果里的目标目录项。
|
||||||
|
"""
|
||||||
|
chain = make_transfer_chain()
|
||||||
|
chain.eventmanager = MagicMock()
|
||||||
|
chain.transfer_completed = lambda *args, **kwargs: None
|
||||||
|
|
||||||
|
task = make_task(1)
|
||||||
|
task.mediainfo = FakeMedia()
|
||||||
|
task.background = False
|
||||||
|
task.manual = True
|
||||||
|
self.assertTrue(chain._TransferChain__put_to_jobview(task))
|
||||||
|
|
||||||
|
target_diritem = FileItem(
|
||||||
|
storage="alist",
|
||||||
|
path="/library/Test Show (2026)/Season 1/",
|
||||||
|
type="dir",
|
||||||
|
name="Season 1",
|
||||||
|
)
|
||||||
|
target_item = FileItem(
|
||||||
|
storage="alist",
|
||||||
|
path="/library/Test Show (2026)/Season 1/Test.Show.S01E01.mkv",
|
||||||
|
type="file",
|
||||||
|
name="Test.Show.S01E01.mkv",
|
||||||
|
extension="mkv",
|
||||||
|
)
|
||||||
|
transferinfo = TransferInfo(
|
||||||
|
success=True,
|
||||||
|
fileitem=task.fileitem,
|
||||||
|
target_item=target_item,
|
||||||
|
target_diritem=target_diritem,
|
||||||
|
file_list_new=[target_item.path],
|
||||||
|
transfer_type="copy",
|
||||||
|
need_scrape=True,
|
||||||
|
need_notify=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.chain.transfer.TransferHistoryOper",
|
||||||
|
return_value=SimpleNamespace(add_success=lambda **kwargs: SimpleNamespace(id=1)),
|
||||||
|
):
|
||||||
|
state, errmsg = chain._TransferChain__default_callback(task, transferinfo)
|
||||||
|
|
||||||
|
self.assertTrue(state)
|
||||||
|
self.assertEqual("", errmsg)
|
||||||
|
metadata_calls = [
|
||||||
|
call
|
||||||
|
for call in chain.eventmanager.send_event.call_args_list
|
||||||
|
if call.args[0] == EventType.MetadataScrape
|
||||||
|
]
|
||||||
|
self.assertEqual(1, len(metadata_calls))
|
||||||
|
event_data = metadata_calls[0].args[1]
|
||||||
|
self.assertEqual(target_diritem, event_data["fileitem"])
|
||||||
|
self.assertEqual([target_item.path], event_data["file_list"])
|
||||||
|
|
||||||
def test_manual_episode_offset_applies_once(self):
|
def test_manual_episode_offset_applies_once(self):
|
||||||
chain = make_transfer_chain()
|
chain = make_transfer_chain()
|
||||||
source_fileitem = make_fileitem("/downloads/Test.Show.2026.S01E14.mkv")
|
source_fileitem = make_fileitem("/downloads/Test.Show.2026.S01E14.mkv")
|
||||||
@@ -791,56 +935,5 @@ class TransferJobManagerTest(unittest.TestCase):
|
|||||||
event_data["file_list"],
|
event_data["file_list"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_success_callback_handles_missing_target_diritem(self):
|
|
||||||
"""
|
|
||||||
成功结果缺少目标目录项时,回调不应把已入库任务误判为失败。
|
|
||||||
"""
|
|
||||||
chain = make_transfer_chain()
|
|
||||||
chain.eventmanager = MagicMock()
|
|
||||||
chain.transfer_completed = lambda *args, **kwargs: None
|
|
||||||
|
|
||||||
task = make_task(1)
|
|
||||||
task.mediainfo = FakeMedia()
|
|
||||||
task.background = False
|
|
||||||
task.manual = True
|
|
||||||
self.assertTrue(chain._TransferChain__put_to_jobview(task))
|
|
||||||
|
|
||||||
target_item = FileItem(
|
|
||||||
storage="alist",
|
|
||||||
path="/library/Test Show (2026)/Season 1/Test.Show.S01E01.mkv",
|
|
||||||
type="file",
|
|
||||||
name="Test.Show.S01E01.mkv",
|
|
||||||
extension="mkv",
|
|
||||||
)
|
|
||||||
transferinfo = TransferInfo(
|
|
||||||
success=True,
|
|
||||||
fileitem=task.fileitem,
|
|
||||||
target_item=target_item,
|
|
||||||
file_list_new=[target_item.path],
|
|
||||||
transfer_type="copy",
|
|
||||||
need_scrape=True,
|
|
||||||
need_notify=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"app.chain.transfer.TransferHistoryOper",
|
|
||||||
return_value=SimpleNamespace(add_success=lambda **kwargs: SimpleNamespace(id=1)),
|
|
||||||
):
|
|
||||||
state, errmsg = chain._TransferChain__default_callback(task, transferinfo)
|
|
||||||
|
|
||||||
self.assertTrue(state)
|
|
||||||
self.assertEqual("", errmsg)
|
|
||||||
metadata_calls = [
|
|
||||||
call
|
|
||||||
for call in chain.eventmanager.send_event.call_args_list
|
|
||||||
if call.args[0] == EventType.MetadataScrape
|
|
||||||
]
|
|
||||||
self.assertEqual(1, len(metadata_calls))
|
|
||||||
event_data = metadata_calls[0].args[1]
|
|
||||||
self.assertEqual("alist", event_data["fileitem"].storage)
|
|
||||||
self.assertEqual("/library/Test Show (2026)/Season 1", event_data["fileitem"].path)
|
|
||||||
self.assertEqual([target_item.path], event_data["file_list"])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user