mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
fix(transfer): support nested download categories (#6156)
This commit is contained in:
+31
-2
@@ -782,6 +782,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""初始化文件整理处理链。"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# 主要媒体文件后缀
|
# 主要媒体文件后缀
|
||||||
self._media_exts = settings.RMT_MEDIAEXT
|
self._media_exts = settings.RMT_MEDIAEXT
|
||||||
@@ -841,6 +842,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
logger.info("文件整理线程已停止")
|
logger.info("文件整理线程已停止")
|
||||||
|
|
||||||
def on_config_changed(self):
|
def on_config_changed(self):
|
||||||
|
"""配置变更时重启文件整理线程。"""
|
||||||
self.__stop()
|
self.__stop()
|
||||||
self.__init()
|
self.__init()
|
||||||
|
|
||||||
@@ -2213,6 +2215,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
shared_roots: set[str] = set()
|
shared_roots: set[str] = set()
|
||||||
media_type_dirs = {mtype.value for mtype in MediaType}
|
media_type_dirs = {mtype.value for mtype in MediaType}
|
||||||
|
media_categories = None
|
||||||
|
|
||||||
for dir_info in DirectoryHelper().get_download_dirs():
|
for dir_info in DirectoryHelper().get_download_dirs():
|
||||||
if not dir_info.download_path:
|
if not dir_info.download_path:
|
||||||
@@ -2226,6 +2229,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
relative_parts = file_path.relative_to(download_root).parts
|
relative_parts = file_path.relative_to(download_root).parts
|
||||||
current_root = download_root
|
current_root = download_root
|
||||||
part_index = 0
|
part_index = 0
|
||||||
|
media_type = dir_info.media_type
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not dir_info.media_type
|
not dir_info.media_type
|
||||||
@@ -2235,6 +2239,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
):
|
):
|
||||||
current_root = current_root / relative_parts[part_index]
|
current_root = current_root / relative_parts[part_index]
|
||||||
shared_roots.add(current_root.as_posix())
|
shared_roots.add(current_root.as_posix())
|
||||||
|
media_type = relative_parts[part_index]
|
||||||
part_index += 1
|
part_index += 1
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -2242,8 +2247,32 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
and dir_info.download_category_folder
|
and dir_info.download_category_folder
|
||||||
and len(relative_parts) > part_index
|
and len(relative_parts) > part_index
|
||||||
):
|
):
|
||||||
current_root = current_root / relative_parts[part_index]
|
category_root = current_root / relative_parts[part_index]
|
||||||
shared_roots.add(current_root.as_posix())
|
shared_roots.add(category_root.as_posix())
|
||||||
|
if media_categories is None:
|
||||||
|
media_categories = MediaChain().media_category() or {}
|
||||||
|
if media_type:
|
||||||
|
category_names = media_categories.get(media_type, [])
|
||||||
|
else:
|
||||||
|
category_names = {
|
||||||
|
category
|
||||||
|
for categories in media_categories.values()
|
||||||
|
for category in categories
|
||||||
|
}
|
||||||
|
category_paths = sorted(
|
||||||
|
(Path(category).parts for category in category_names if category),
|
||||||
|
key=len,
|
||||||
|
)
|
||||||
|
for category_parts in category_paths:
|
||||||
|
relative_category_parts = tuple(
|
||||||
|
relative_parts[part_index:part_index + len(category_parts)]
|
||||||
|
)
|
||||||
|
if relative_category_parts != category_parts:
|
||||||
|
continue
|
||||||
|
category_root = current_root
|
||||||
|
for category_part in category_parts:
|
||||||
|
category_root = category_root / category_part
|
||||||
|
shared_roots.add(category_root.as_posix())
|
||||||
|
|
||||||
return shared_roots
|
return shared_roots
|
||||||
|
|
||||||
|
|||||||
@@ -1,213 +1,308 @@
|
|||||||
import unittest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from app.chain.transfer import TransferChain
|
from app.chain.transfer import TransferChain
|
||||||
|
|
||||||
|
|
||||||
class FakeDownloadHistoryOper:
|
class FakeDownloadHistoryOper:
|
||||||
|
"""提供下载历史回查测试所需的内存桩。"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
histories_by_hash=None,
|
histories_by_hash=None,
|
||||||
histories_by_path=None,
|
histories_by_path=None,
|
||||||
files_by_fullpath=None,
|
files_by_fullpath=None,
|
||||||
files_by_savepath=None,
|
files_by_savepath=None,
|
||||||
):
|
):
|
||||||
|
"""初始化各查询维度的测试数据。"""
|
||||||
self.histories_by_hash = histories_by_hash or {}
|
self.histories_by_hash = histories_by_hash or {}
|
||||||
self.histories_by_path = histories_by_path or {}
|
self.histories_by_path = histories_by_path or {}
|
||||||
self.files_by_fullpath = files_by_fullpath or {}
|
self.files_by_fullpath = files_by_fullpath or {}
|
||||||
self.files_by_savepath = files_by_savepath or {}
|
self.files_by_savepath = files_by_savepath or {}
|
||||||
|
|
||||||
def get_by_hash(self, download_hash: str):
|
def get_by_hash(self, download_hash: str):
|
||||||
|
"""按下载哈希返回历史。"""
|
||||||
return self.histories_by_hash.get(download_hash)
|
return self.histories_by_hash.get(download_hash)
|
||||||
|
|
||||||
def get_by_path(self, path: str):
|
def get_by_path(self, path: str):
|
||||||
|
"""按下载路径返回历史。"""
|
||||||
return self.histories_by_path.get(path)
|
return self.histories_by_path.get(path)
|
||||||
|
|
||||||
def get_file_by_fullpath(self, fullpath: str):
|
def get_file_by_fullpath(self, fullpath: str):
|
||||||
|
"""按完整文件路径返回下载文件记录。"""
|
||||||
return self.files_by_fullpath.get(fullpath)
|
return self.files_by_fullpath.get(fullpath)
|
||||||
|
|
||||||
def get_files_by_savepath(self, savepath: str):
|
def get_files_by_savepath(self, savepath: str):
|
||||||
|
"""按保存路径返回下载文件记录。"""
|
||||||
return self.files_by_savepath.get(savepath, [])
|
return self.files_by_savepath.get(savepath, [])
|
||||||
|
|
||||||
|
|
||||||
class TransferDownloadHistoryLookupTest(unittest.TestCase):
|
def _make_chain() -> TransferChain:
|
||||||
def setUp(self):
|
"""构造不启动后台线程的整理链实例。"""
|
||||||
self.chain = object.__new__(TransferChain)
|
return object.__new__(TransferChain)
|
||||||
|
|
||||||
def test_resolve_download_history_falls_back_to_parent_download_path(self):
|
|
||||||
expected = SimpleNamespace(download_hash="hash1", downloader="qb")
|
|
||||||
oper = FakeDownloadHistoryOper(
|
|
||||||
histories_by_hash={"hash1": expected},
|
|
||||||
histories_by_path={"/downloads/season-pack": expected},
|
|
||||||
)
|
|
||||||
|
|
||||||
history = self.chain._resolve_download_history(
|
def _download_dir(**overrides):
|
||||||
downloadhis=oper,
|
"""构造下载目录配置桩。"""
|
||||||
file_path=Path("/downloads/season-pack/Test.Show.S01E01.mkv"),
|
values = {
|
||||||
)
|
"download_path": "/downloads",
|
||||||
|
"media_type": None,
|
||||||
|
"download_type_folder": False,
|
||||||
|
"media_category": None,
|
||||||
|
"download_category_folder": False,
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return SimpleNamespace(**values)
|
||||||
|
|
||||||
self.assertIs(history, expected)
|
|
||||||
|
|
||||||
def test_resolve_download_history_falls_back_to_unique_savepath_hash(self):
|
def test_resolve_download_history_falls_back_to_parent_download_path():
|
||||||
expected = SimpleNamespace(download_hash="hash1", downloader="qb")
|
"""文件记录缺失时应按种子父目录回查下载历史。"""
|
||||||
oper = FakeDownloadHistoryOper(
|
expected = SimpleNamespace(download_hash="hash1", downloader="qb")
|
||||||
histories_by_hash={"hash1": expected},
|
oper = FakeDownloadHistoryOper(
|
||||||
files_by_savepath={
|
histories_by_hash={"hash1": expected},
|
||||||
"/downloads/season-pack": [
|
histories_by_path={"/downloads/season-pack": expected},
|
||||||
SimpleNamespace(download_hash="hash1"),
|
)
|
||||||
SimpleNamespace(download_hash="hash1"),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
history = self.chain._resolve_download_history(
|
history = _make_chain()._resolve_download_history(
|
||||||
downloadhis=oper,
|
downloadhis=oper,
|
||||||
file_path=Path("/downloads/season-pack/subs/Test.Show.S01E01.zh.ass"),
|
file_path=Path("/downloads/season-pack/Test.Show.S01E01.mkv"),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIs(history, expected)
|
assert history is expected
|
||||||
|
|
||||||
def test_resolve_download_history_skips_ambiguous_savepath_hashes(self):
|
|
||||||
oper = FakeDownloadHistoryOper(
|
|
||||||
histories_by_hash={
|
|
||||||
"hash1": SimpleNamespace(download_hash="hash1", downloader="qb"),
|
|
||||||
"hash2": SimpleNamespace(download_hash="hash2", downloader="tr"),
|
|
||||||
},
|
|
||||||
files_by_savepath={
|
|
||||||
"/downloads/shared": [
|
|
||||||
SimpleNamespace(download_hash="hash1"),
|
|
||||||
SimpleNamespace(download_hash="hash2"),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
history = self.chain._resolve_download_history(
|
def test_resolve_download_history_falls_back_to_unique_savepath_hash():
|
||||||
downloadhis=oper,
|
"""父目录只有一个下载哈希时应返回对应历史。"""
|
||||||
file_path=Path("/downloads/shared/Test.Show.S01E01.mkv"),
|
expected = SimpleNamespace(download_hash="hash1", downloader="qb")
|
||||||
)
|
oper = FakeDownloadHistoryOper(
|
||||||
|
histories_by_hash={"hash1": expected},
|
||||||
|
files_by_savepath={
|
||||||
|
"/downloads/season-pack": [
|
||||||
|
SimpleNamespace(download_hash="hash1"),
|
||||||
|
SimpleNamespace(download_hash="hash1"),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertIsNone(history)
|
history = _make_chain()._resolve_download_history(
|
||||||
|
downloadhis=oper,
|
||||||
|
file_path=Path("/downloads/season-pack/subs/Test.Show.S01E01.zh.ass"),
|
||||||
|
)
|
||||||
|
|
||||||
def test_resolve_download_history_stops_at_shared_download_root_path(self):
|
assert history is expected
|
||||||
oper = FakeDownloadHistoryOper(
|
|
||||||
histories_by_path={
|
|
||||||
"/downloads": SimpleNamespace(download_hash="hash1", downloader="qb")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
def test_resolve_download_history_skips_ambiguous_savepath_hashes():
|
||||||
return_value=[
|
"""父目录关联多个下载哈希时不应猜测下载历史。"""
|
||||||
|
oper = FakeDownloadHistoryOper(
|
||||||
|
histories_by_hash={
|
||||||
|
"hash1": SimpleNamespace(download_hash="hash1", downloader="qb"),
|
||||||
|
"hash2": SimpleNamespace(download_hash="hash2", downloader="tr"),
|
||||||
|
},
|
||||||
|
files_by_savepath={
|
||||||
|
"/downloads/shared": [
|
||||||
|
SimpleNamespace(download_hash="hash1"),
|
||||||
|
SimpleNamespace(download_hash="hash2"),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
history = _make_chain()._resolve_download_history(
|
||||||
|
downloadhis=oper,
|
||||||
|
file_path=Path("/downloads/shared/Test.Show.S01E01.mkv"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert history is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_download_history_stops_at_shared_download_root_path(monkeypatch):
|
||||||
|
"""共享下载根目录上的路径历史不应污染同级文件。"""
|
||||||
|
oper = FakeDownloadHistoryOper(
|
||||||
|
histories_by_path={
|
||||||
|
"/downloads": SimpleNamespace(download_hash="hash1", downloader="qb")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
||||||
|
lambda _: [_download_dir()],
|
||||||
|
)
|
||||||
|
|
||||||
|
history = _make_chain()._resolve_download_history(
|
||||||
|
downloadhis=oper,
|
||||||
|
file_path=Path("/downloads/Ghost.Concert.mkv"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert history is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_download_history_stops_at_shared_download_root_savepath(monkeypatch):
|
||||||
|
"""共享下载根目录上的其它文件记录不应污染当前文件。"""
|
||||||
|
expected = SimpleNamespace(download_hash="hash1", downloader="qb")
|
||||||
|
oper = FakeDownloadHistoryOper(
|
||||||
|
histories_by_hash={"hash1": expected},
|
||||||
|
files_by_savepath={
|
||||||
|
"/downloads": [
|
||||||
SimpleNamespace(
|
SimpleNamespace(
|
||||||
download_path="/downloads",
|
download_hash="hash1",
|
||||||
media_type=None,
|
fullpath="/downloads/Other.Show.mkv",
|
||||||
download_type_folder=False,
|
filepath="Other.Show.mkv",
|
||||||
media_category=None,
|
),
|
||||||
download_category_folder=False,
|
]
|
||||||
)
|
},
|
||||||
],
|
)
|
||||||
):
|
monkeypatch.setattr(
|
||||||
history = self.chain._resolve_download_history(
|
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
||||||
downloadhis=oper,
|
lambda _: [_download_dir()],
|
||||||
file_path=Path("/downloads/Ghost.Concert.mkv"),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIsNone(history)
|
history = _make_chain()._resolve_download_history(
|
||||||
|
downloadhis=oper,
|
||||||
|
file_path=Path("/downloads/Ghost.Concert.mkv"),
|
||||||
|
)
|
||||||
|
|
||||||
def test_resolve_download_history_stops_at_shared_download_root_savepath(self):
|
assert history is None
|
||||||
expected = SimpleNamespace(download_hash="hash1", downloader="qb")
|
|
||||||
oper = FakeDownloadHistoryOper(
|
|
||||||
histories_by_hash={"hash1": expected},
|
|
||||||
files_by_savepath={
|
|
||||||
"/downloads": [
|
|
||||||
SimpleNamespace(
|
|
||||||
download_hash="hash1",
|
|
||||||
fullpath="/downloads/Other.Show.mkv",
|
|
||||||
filepath="Other.Show.mkv",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
def test_resolve_download_history_accepts_shared_root_savepath_for_exact_file(monkeypatch):
|
||||||
return_value=[
|
"""共享根目录存在当前文件的明确记录时应允许命中。"""
|
||||||
|
expected = SimpleNamespace(download_hash="hash1", downloader="qb")
|
||||||
|
oper = FakeDownloadHistoryOper(
|
||||||
|
histories_by_hash={"hash1": expected},
|
||||||
|
files_by_savepath={
|
||||||
|
"/downloads": [
|
||||||
SimpleNamespace(
|
SimpleNamespace(
|
||||||
download_path="/downloads",
|
download_hash="hash1",
|
||||||
media_type=None,
|
fullpath="/downloads/Ghost.Concert.mkv",
|
||||||
download_type_folder=False,
|
filepath="Ghost.Concert.mkv",
|
||||||
media_category=None,
|
),
|
||||||
download_category_folder=False,
|
]
|
||||||
)
|
},
|
||||||
],
|
)
|
||||||
):
|
monkeypatch.setattr(
|
||||||
history = self.chain._resolve_download_history(
|
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
||||||
downloadhis=oper,
|
lambda _: [_download_dir()],
|
||||||
file_path=Path("/downloads/Ghost.Concert.mkv"),
|
)
|
||||||
|
|
||||||
|
history = _make_chain()._resolve_download_history(
|
||||||
|
downloadhis=oper,
|
||||||
|
file_path=Path("/downloads/Ghost.Concert.mkv"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert history is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_download_history_stops_at_type_category_download_root(monkeypatch):
|
||||||
|
"""按类型和类别生成的共享目录不应回查该目录自身的历史。"""
|
||||||
|
oper = FakeDownloadHistoryOper(
|
||||||
|
histories_by_path={
|
||||||
|
"/downloads/电视剧/动漫": SimpleNamespace(
|
||||||
|
download_hash="hash1", downloader="qb"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
self.assertIsNone(history)
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
def test_resolve_download_history_accepts_shared_root_savepath_for_exact_file(self):
|
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
||||||
expected = SimpleNamespace(download_hash="hash1", downloader="qb")
|
lambda _: [
|
||||||
oper = FakeDownloadHistoryOper(
|
_download_dir(
|
||||||
histories_by_hash={"hash1": expected},
|
download_type_folder=True,
|
||||||
files_by_savepath={
|
download_category_folder=True,
|
||||||
"/downloads": [
|
|
||||||
SimpleNamespace(
|
|
||||||
download_hash="hash1",
|
|
||||||
fullpath="/downloads/Ghost.Concert.mkv",
|
|
||||||
filepath="Ghost.Concert.mkv",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
|
||||||
return_value=[
|
|
||||||
SimpleNamespace(
|
|
||||||
download_path="/downloads",
|
|
||||||
media_type=None,
|
|
||||||
download_type_folder=False,
|
|
||||||
media_category=None,
|
|
||||||
download_category_folder=False,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
):
|
|
||||||
history = self.chain._resolve_download_history(
|
|
||||||
downloadhis=oper,
|
|
||||||
file_path=Path("/downloads/Ghost.Concert.mkv"),
|
|
||||||
)
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.chain.transfer.MediaChain.media_category",
|
||||||
|
lambda _: {"电影": [], "电视剧": ["动漫"]},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertIs(history, expected)
|
history = _make_chain()._resolve_download_history(
|
||||||
|
downloadhis=oper,
|
||||||
|
file_path=Path("/downloads/电视剧/动漫/Ghost.Concert.mkv"),
|
||||||
|
)
|
||||||
|
|
||||||
def test_resolve_download_history_stops_at_type_category_download_root(self):
|
assert history is None
|
||||||
oper = FakeDownloadHistoryOper(
|
|
||||||
histories_by_path={
|
|
||||||
"/downloads/电视剧/动漫": SimpleNamespace(
|
|
||||||
download_hash="hash1", downloader="qb"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
def test_get_shared_download_roots_includes_nested_category(monkeypatch):
|
||||||
return_value=[
|
"""多级分类的每一级目录都应成为共享下载边界。"""
|
||||||
SimpleNamespace(
|
monkeypatch.setattr(
|
||||||
download_path="/downloads",
|
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
||||||
media_type=None,
|
lambda _: [_download_dir(download_category_folder=True)],
|
||||||
download_type_folder=True,
|
)
|
||||||
media_category=None,
|
monkeypatch.setattr(
|
||||||
download_category_folder=True,
|
"app.chain.transfer.MediaChain.media_category",
|
||||||
)
|
lambda _: {"电影": [], "电视剧": ["动漫/日本/季度新番"]},
|
||||||
],
|
)
|
||||||
):
|
|
||||||
history = self.chain._resolve_download_history(
|
roots = TransferChain._get_shared_download_roots(
|
||||||
downloadhis=oper,
|
Path("/downloads/动漫/日本/季度新番/Show.S01E01.mkv")
|
||||||
file_path=Path("/downloads/电视剧/动漫/Ghost.Concert.mkv"),
|
)
|
||||||
|
|
||||||
|
assert roots == {
|
||||||
|
"/downloads",
|
||||||
|
"/downloads/动漫",
|
||||||
|
"/downloads/动漫/日本",
|
||||||
|
"/downloads/动漫/日本/季度新番",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_shared_download_roots_excludes_torrent_subdirectory(monkeypatch):
|
||||||
|
"""分类目录下由种子创建的子目录不应成为共享下载边界。"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
||||||
|
lambda _: [_download_dir(download_category_folder=True)],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.chain.transfer.MediaChain.media_category",
|
||||||
|
lambda _: {"电影": [], "电视剧": ["动漫/日本番剧"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
roots = TransferChain._get_shared_download_roots(
|
||||||
|
Path("/downloads/动漫/日本番剧/Torrent.Name/Show.S01E01.mkv")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "/downloads/动漫/日本番剧" in roots
|
||||||
|
assert "/downloads/动漫/日本番剧/Torrent.Name" not in roots
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_shared_download_roots_keeps_first_level_without_category_config(monkeypatch):
|
||||||
|
"""分类配置不可用时应保留原有的一级共享边界保护。"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
||||||
|
lambda _: [_download_dir(download_category_folder=True)],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.chain.transfer.MediaChain.media_category",
|
||||||
|
lambda _: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
roots = TransferChain._get_shared_download_roots(
|
||||||
|
Path("/downloads/动漫/Torrent.Name/Show.S01E01.mkv")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert roots == {"/downloads", "/downloads/动漫"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_download_history_stops_at_nested_category_root(monkeypatch):
|
||||||
|
"""多级分类叶子目录中的其它任务历史不应污染当前文件。"""
|
||||||
|
oper = FakeDownloadHistoryOper(
|
||||||
|
histories_by_path={
|
||||||
|
"/downloads/动漫/日本番剧": SimpleNamespace(
|
||||||
|
download_hash="other-hash", downloader="qb"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.chain.transfer.DirectoryHelper.get_download_dirs",
|
||||||
|
lambda _: [_download_dir(download_category_folder=True)],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.chain.transfer.MediaChain.media_category",
|
||||||
|
lambda _: {"电影": [], "电视剧": ["动漫/日本番剧"]},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertIsNone(history)
|
history = _make_chain()._resolve_download_history(
|
||||||
|
downloadhis=oper,
|
||||||
|
file_path=Path("/downloads/动漫/日本番剧/Ghost.Concert.mkv"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert history is None
|
||||||
|
|||||||
Reference in New Issue
Block a user