mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
fix(transfer): guard missing episode sample paths and type recommendations
This commit is contained in:
@@ -6,7 +6,7 @@ from typing import List, Optional, Tuple, Union, cast
|
||||
from app.application.configuration import (
|
||||
get_configured_system_config,
|
||||
)
|
||||
from app.application.formatting import EpisodeFormatRuleHelper
|
||||
from app.application.formatting import EpisodeFormatRecommendation, EpisodeFormatRuleHelper
|
||||
from app.application.history.retry import max_failed_retries
|
||||
from app.application.transfer.feedback import TransferFailureNotification, classify_transfer_failure
|
||||
from app.application.transfer.workflow import TransferTask
|
||||
@@ -81,7 +81,7 @@ class EpisodeFormatMixin(_TransferOwnerBase):
|
||||
self,
|
||||
fileitem: Optional[FileItem],
|
||||
fileitems: Optional[List[FileItem]] = None,
|
||||
) -> Tuple[bool, str, Optional[dict]]:
|
||||
) -> Tuple[bool, str, Optional[EpisodeFormatRecommendation]]:
|
||||
"""
|
||||
根据目录样本推荐集数定位模板
|
||||
"""
|
||||
@@ -148,6 +148,8 @@ class EpisodeFormatMixin(_TransferOwnerBase):
|
||||
"""
|
||||
将文件或目录入参归一化为目录对象
|
||||
"""
|
||||
if not fileitem.path:
|
||||
return None
|
||||
storage_chain = StorageChain()
|
||||
if fileitem.type == "dir":
|
||||
return storage_chain.get_item(fileitem)
|
||||
|
||||
56
tests/test_episode_format_contract.py
Normal file
56
tests/test_episode_format_contract.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""集数范围筛选与缺失文件名的回归边界。"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.formatting import EpisodeFormatRuleHelper, FormatParser
|
||||
from app.chain.transfer.format import EpisodeFormatMixin
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.schemas.file import FileItem
|
||||
from app.schemas.transfer import EpisodeFormatRule
|
||||
|
||||
|
||||
@pytest.mark.parametrize("episode,expected", [(0, False), (2, False), (3, True), (4, True), (5, False)])
|
||||
def test_hyphen_range_matches_numeric_boundaries_before_offset(episode, expected):
|
||||
"""连字符区间按数字边界筛选,集数偏移不应改变筛选范围或原始属性。"""
|
||||
parser = FormatParser(eformat="Show - {ep}.mkv", details="03-04", offset="EP+10")
|
||||
assert parser.start_ep == parser.end_ep == "03-04"
|
||||
assert parser.match(f"Show - {episode:02d}.mkv") is expected
|
||||
assert parser.split_episode("Show - 03.mkv", MetaBase(title="")) == (13, 14, None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("details,episode,expected", [
|
||||
("3", 2, False), ("3", 3, True), ("3,4", 3, True), ("3,4", 4, True),
|
||||
("4,3", 3, False), ("4,3", 4, True), ("4-3", 3, False), ("4-3", 4, False),
|
||||
])
|
||||
def test_existing_single_and_comma_range_rules_keep_boundaries(details, episode, expected):
|
||||
"""单集与逗号区间保留既有范围规则,倒序连字符区间不自动交换边界。"""
|
||||
parser = FormatParser(eformat="Show - {ep}.mkv", details=details)
|
||||
assert parser.match(f"Show - {episode:02d}.mkv") is expected
|
||||
|
||||
|
||||
def test_missing_file_name_cannot_crash_template_recommendation(monkeypatch):
|
||||
"""正则允许空集数时,缺少文件名的样本仍应正常返回推荐失败。"""
|
||||
monkeypatch.setattr(
|
||||
"app.application.formatting.get_chain_runtime_config_snapshot",
|
||||
lambda: SimpleNamespace(video_extensions=(".mkv",), subtitle_extensions=(), audio_extensions=()),
|
||||
)
|
||||
sample = FileItem(storage="local", path="/downloads/", type="file", name=None, extension="mkv")
|
||||
rule = EpisodeFormatRule(name="empty", pattern=r"(?<ep>\d*)")
|
||||
state, message, data = EpisodeFormatRuleHelper().recommend([rule], [sample])
|
||||
assert state is False
|
||||
assert message
|
||||
assert data is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", [None, ""])
|
||||
def test_missing_sample_path_does_not_resolve_current_directory(monkeypatch, path):
|
||||
"""无路径的样本不能被当成当前目录,也不能触发存储查询。"""
|
||||
storage = Mock(side_effect=AssertionError("缺少路径时不得访问存储"))
|
||||
monkeypatch.setattr("app.chain.transfer.format.StorageChain", storage)
|
||||
source = FileItem(storage="local", path=path, type="file", name="show.mkv")
|
||||
owner = object.__new__(EpisodeFormatMixin)
|
||||
assert owner._resolve_episode_format_directory(source) is None
|
||||
storage.assert_not_called()
|
||||
Reference in New Issue
Block a user