mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 00:16:57 +08:00
fix: tolerate delayed OpenList metadata
This commit is contained in:
@@ -331,7 +331,18 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._delay_get_item(path, refresh=True)
|
return self._delay_get_item(
|
||||||
|
path, refresh=True
|
||||||
|
) or self.__build_transfer_item(
|
||||||
|
schemas.FileItem(
|
||||||
|
storage=self.schema.value,
|
||||||
|
type="dir",
|
||||||
|
path=fileitem.path,
|
||||||
|
name=name,
|
||||||
|
basename=Path(name).stem,
|
||||||
|
),
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
|
||||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -546,6 +546,7 @@ class TransHandler:
|
|||||||
result=result,
|
result=result,
|
||||||
)
|
)
|
||||||
if not new_item:
|
if not new_item:
|
||||||
|
err_msg = err_msg or f"{fileitem.path} 整理后未获取到目标文件信息"
|
||||||
logger.error(f"文件 {fileitem.path} 整理失败:{err_msg}")
|
logger.error(f"文件 {fileitem.path} 整理失败:{err_msg}")
|
||||||
self.__update_result(
|
self.__update_result(
|
||||||
result=result,
|
result=result,
|
||||||
@@ -608,6 +609,34 @@ class TransHandler:
|
|||||||
modify_time=_path.stat().st_mtime,
|
modify_time=_path.stat().st_mtime,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __build_remote_targetitem(_source_item: FileItem, _path: Path) -> FileItem:
|
||||||
|
"""
|
||||||
|
根据已确认的目标路径构造网盘文件信息,用于兼容元数据延迟可见的存储。
|
||||||
|
"""
|
||||||
|
return FileItem(
|
||||||
|
storage=target_storage,
|
||||||
|
path=_path.as_posix(),
|
||||||
|
name=_path.name,
|
||||||
|
basename=_path.stem,
|
||||||
|
type=_source_item.type or "file",
|
||||||
|
size=_source_item.size,
|
||||||
|
extension=_path.suffix.lstrip("."),
|
||||||
|
modify_time=_source_item.modify_time,
|
||||||
|
thumbnail=_source_item.thumbnail,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __get_remote_targetitem(_source_item: FileItem, _path: Path) -> FileItem:
|
||||||
|
"""
|
||||||
|
获取网盘目标文件信息,目标存储索引未刷新时使用目标路径兜底。
|
||||||
|
"""
|
||||||
|
target_item = target_oper.get_item(_path)
|
||||||
|
if target_item:
|
||||||
|
return target_item
|
||||||
|
logger.warn(
|
||||||
|
f"目标文件【{target_storage}】{_path} 元数据暂不可见,使用目标路径构造整理结果"
|
||||||
|
)
|
||||||
|
return __build_remote_targetitem(_source_item, _path)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
fileitem.storage != target_storage
|
fileitem.storage != target_storage
|
||||||
and fileitem.storage != "local"
|
and fileitem.storage != "local"
|
||||||
@@ -719,9 +748,7 @@ class TransHandler:
|
|||||||
elif source_oper.copy(
|
elif source_oper.copy(
|
||||||
fileitem, Path(target_fileitem.path), target_file.name
|
fileitem, Path(target_fileitem.path), target_file.name
|
||||||
):
|
):
|
||||||
new_item = target_oper.get_item(target_file)
|
return __get_remote_targetitem(fileitem, target_file), ""
|
||||||
if new_item:
|
|
||||||
return new_item, ""
|
|
||||||
return None, f"【{target_storage}】{fileitem.path} 复制文件失败"
|
return None, f"【{target_storage}】{fileitem.path} 复制文件失败"
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
@@ -742,9 +769,7 @@ class TransHandler:
|
|||||||
elif source_oper.move(
|
elif source_oper.move(
|
||||||
fileitem, Path(target_fileitem.path), target_file.name
|
fileitem, Path(target_fileitem.path), target_file.name
|
||||||
):
|
):
|
||||||
new_item = target_oper.get_item(target_file)
|
return __get_remote_targetitem(fileitem, target_file), ""
|
||||||
if new_item:
|
|
||||||
return new_item, ""
|
|
||||||
return None, f"【{target_storage}】{fileitem.path} 移动文件失败"
|
return None, f"【{target_storage}】{fileitem.path} 移动文件失败"
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
@@ -753,7 +778,7 @@ class TransHandler:
|
|||||||
)
|
)
|
||||||
elif transfer_type == "link":
|
elif transfer_type == "link":
|
||||||
if source_oper.link(fileitem, target_file):
|
if source_oper.link(fileitem, target_file):
|
||||||
return target_oper.get_item(target_file), ""
|
return __get_remote_targetitem(fileitem, target_file), ""
|
||||||
else:
|
else:
|
||||||
return None, f"【{target_storage}】{fileitem.path} 创建硬链接失败"
|
return None, f"【{target_storage}】{fileitem.path} 创建硬链接失败"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -200,6 +200,29 @@ class AlistStorageTest(unittest.TestCase):
|
|||||||
self.assertEqual(50, len(items))
|
self.assertEqual(50, len(items))
|
||||||
self.assertEqual(1, request_utils.post_res.call_count)
|
self.assertEqual(1, request_utils.post_res.call_count)
|
||||||
|
|
||||||
|
def test_create_folder_returns_target_when_openlist_metadata_is_delayed(self):
|
||||||
|
"""
|
||||||
|
OpenList 创建目录成功但元数据延迟可见时,应返回可用的目标目录项。
|
||||||
|
"""
|
||||||
|
request_utils = MagicMock()
|
||||||
|
request_utils.post_res.return_value = _FakeResponse(
|
||||||
|
{"code": 200, "message": "success", "data": None}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(Alist, "get_conf", return_value={"url": "http://openlist.test", "token": "token"}):
|
||||||
|
with patch.object(self.storage, "_Alist__get_header_with_token", return_value={}):
|
||||||
|
with patch.object(alist_module, "RequestUtils", return_value=request_utils):
|
||||||
|
with patch.object(self.storage, "_delay_get_item", return_value=None):
|
||||||
|
folder = self.storage.create_folder(
|
||||||
|
self._dir_item("/library/Test Show (2026)"),
|
||||||
|
"Season 1",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(folder)
|
||||||
|
self.assertEqual("/library/Test Show (2026)/Season 1", folder.path)
|
||||||
|
self.assertEqual("alist", folder.storage)
|
||||||
|
self.assertEqual("dir", folder.type)
|
||||||
|
|
||||||
def test_move_item_returns_target_when_openlist_metadata_is_delayed(self):
|
def test_move_item_returns_target_when_openlist_metadata_is_delayed(self):
|
||||||
"""
|
"""
|
||||||
OpenList 操作成功但目标元数据延迟可见时,应返回可用的目标文件项。
|
OpenList 操作成功但目标元数据延迟可见时,应返回可用的目标文件项。
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock
|
|||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.chain.transfer import JobManager, TransferChain
|
from app.chain.transfer import JobManager, TransferChain
|
||||||
|
from app.modules.filemanager.transhandler import TransHandler
|
||||||
from app.schemas import EpisodeFormat, FileItem, TransferInfo, TransferTask
|
from app.schemas import EpisodeFormat, FileItem, TransferInfo, TransferTask
|
||||||
from app.schemas.types import EventType, MediaType
|
from app.schemas.types import EventType, MediaType
|
||||||
|
|
||||||
@@ -127,6 +128,54 @@ def migrate_to_media_job(jobview: JobManager, task: TransferTask):
|
|||||||
|
|
||||||
|
|
||||||
class TransferJobManagerTest(unittest.TestCase):
|
class TransferJobManagerTest(unittest.TestCase):
|
||||||
|
def test_same_storage_success_uses_target_path_when_metadata_is_delayed(self):
|
||||||
|
"""
|
||||||
|
网盘操作已成功但目标元数据暂不可见时,整理结果应按成功路径落库。
|
||||||
|
"""
|
||||||
|
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/Test Show (2026)/Season 1/Test.Show.S01E01.mkv"
|
||||||
|
)
|
||||||
|
target_folder = FileItem(
|
||||||
|
storage="alist",
|
||||||
|
path=target_path.parent.as_posix(),
|
||||||
|
type="dir",
|
||||||
|
name=target_path.parent.name,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_item, errmsg = TransHandler._TransHandler__transfer_command(
|
||||||
|
fileitem=source_item,
|
||||||
|
target_storage="alist",
|
||||||
|
source_oper=source_oper,
|
||||||
|
target_oper=target_oper,
|
||||||
|
target_file=target_path,
|
||||||
|
transfer_type="move",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("", errmsg)
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(target_path.as_posix(), new_item.path)
|
||||||
|
self.assertEqual("alist", new_item.storage)
|
||||||
|
self.assertEqual("file", new_item.type)
|
||||||
|
self.assertEqual(1024, new_item.size)
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user