mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
fix: avoid double episode offset in manual transfer
This commit is contained in:
@@ -5,38 +5,30 @@ init-hook='import sys; sys.path.append(".")'
|
|||||||
# 忽略的文件和目录
|
# 忽略的文件和目录
|
||||||
ignore=.git,__pycache__,.venv,build,dist,tests,docs
|
ignore=.git,__pycache__,.venv,build,dist,tests,docs
|
||||||
|
|
||||||
|
# 通过 `pylint app/` 检查主程序时不扫描内置插件目录,
|
||||||
|
# 插件依赖和动态模型较多,容易产生与主程序无关的误报。
|
||||||
|
ignore-paths=^app/plugins(/|$)
|
||||||
|
|
||||||
# 并行作业数量
|
# 并行作业数量
|
||||||
jobs=0
|
jobs=0
|
||||||
|
|
||||||
[MESSAGES CONTROL]
|
[MESSAGES CONTROL]
|
||||||
# 只关注错误级别的问题,禁用警告、约定和重构建议
|
# 只启用确定性较强的严重问题检查,避免 SQLAlchemy、FastAPI 依赖注入、
|
||||||
# E = Error (错误) - 会导致构建失败
|
# 第三方 SDK 等动态对象被 Pylint 推断成误报。
|
||||||
# W = Warning (警告) - 仅显示,不会失败
|
|
||||||
# R = Refactor (重构建议) - 仅显示,不会失败
|
|
||||||
# C = Convention (约定) - 仅显示,不会失败
|
|
||||||
# I = Information (信息) - 仅显示,不会失败
|
|
||||||
|
|
||||||
# 禁用大部分警告、约定和重构建议,只保留错误和重要警告
|
|
||||||
disable=all
|
disable=all
|
||||||
enable=E,
|
enable=syntax-error,
|
||||||
syntax-error,
|
|
||||||
undefined-variable,
|
undefined-variable,
|
||||||
used-before-assignment,
|
used-before-assignment,
|
||||||
|
possibly-used-before-assignment,
|
||||||
unreachable,
|
unreachable,
|
||||||
return-outside-function,
|
return-outside-function,
|
||||||
yield-outside-function,
|
yield-outside-function,
|
||||||
continue-in-finally,
|
continue-in-finally,
|
||||||
nonlocal-without-binding,
|
nonlocal-without-binding,
|
||||||
undefined-loop-variable,
|
undefined-loop-variable,
|
||||||
redefined-builtin,
|
|
||||||
not-callable,
|
|
||||||
assignment-from-no-return,
|
|
||||||
no-value-for-parameter,
|
|
||||||
too-many-function-args,
|
|
||||||
unexpected-keyword-arg,
|
|
||||||
redundant-keyword-arg,
|
|
||||||
import-error,
|
import-error,
|
||||||
relative-beyond-top-level
|
relative-beyond-top-level,
|
||||||
|
no-name-in-module
|
||||||
|
|
||||||
[REPORTS]
|
[REPORTS]
|
||||||
# 设置报告格式
|
# 设置报告格式
|
||||||
|
|||||||
@@ -2245,6 +2245,10 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
if not built_meta:
|
if not built_meta:
|
||||||
return None
|
return None
|
||||||
|
if not meta:
|
||||||
|
# _build_path_meta 已经应用过手动季集/自定义格式覆盖;
|
||||||
|
# 这里避免再次偏移集数,导致手动整理的集数偏移翻倍。
|
||||||
|
return built_meta
|
||||||
return _apply_meta_overrides(built_meta, source_path)
|
return _apply_meta_overrides(built_meta, source_path)
|
||||||
|
|
||||||
def _build_path_meta(
|
def _build_path_meta(
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
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.chain.transfer import JobManager, TransferChain
|
from app.chain.transfer import JobManager, TransferChain
|
||||||
from app.schemas import 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
|
||||||
|
|
||||||
|
|
||||||
@@ -126,6 +127,49 @@ def migrate_to_media_job(jobview: JobManager, task: TransferTask):
|
|||||||
|
|
||||||
|
|
||||||
class TransferJobManagerTest(unittest.TestCase):
|
class TransferJobManagerTest(unittest.TestCase):
|
||||||
|
def test_manual_episode_offset_applies_once(self):
|
||||||
|
chain = make_transfer_chain()
|
||||||
|
source_fileitem = make_fileitem("/downloads/Test.Show.2026.S01E14.mkv")
|
||||||
|
planned_episodes = []
|
||||||
|
|
||||||
|
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
|
||||||
|
(source_fileitem, False)
|
||||||
|
]
|
||||||
|
chain._TransferChain__put_to_jobview = lambda task: True
|
||||||
|
chain._TransferChain__register_scrape_batch_task = lambda task: None
|
||||||
|
chain._TransferChain__close_scrape_batch = lambda batch_id: None
|
||||||
|
|
||||||
|
def fake_handle_transfer(task, callback=None):
|
||||||
|
planned_episodes.append(task.meta.begin_episode)
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
chain._TransferChain__handle_transfer = fake_handle_transfer
|
||||||
|
|
||||||
|
transfer_history_oper = SimpleNamespace(get_by_src=lambda src, storage=None: None)
|
||||||
|
download_history_oper = SimpleNamespace(
|
||||||
|
get_by_hash=lambda download_hash: None,
|
||||||
|
get_file_by_fullpath=lambda fullpath: None,
|
||||||
|
get_files_by_savepath=lambda savepath: [],
|
||||||
|
get_by_path=lambda path: None,
|
||||||
|
)
|
||||||
|
system_config_oper = SimpleNamespace(get=lambda key: None)
|
||||||
|
|
||||||
|
with patch("app.chain.transfer.TransferHistoryOper", return_value=transfer_history_oper), \
|
||||||
|
patch("app.chain.transfer.DownloadHistoryOper", return_value=download_history_oper), \
|
||||||
|
patch("app.chain.transfer.SystemConfigOper", return_value=system_config_oper), \
|
||||||
|
patch("app.chain.transfer.MetaInfoPath", lambda *args, **kwargs: FakeMeta(14)):
|
||||||
|
state, errmsg = chain.do_transfer(
|
||||||
|
fileitem=source_fileitem,
|
||||||
|
mediainfo=FakeMedia(),
|
||||||
|
target_path=Path("/library"),
|
||||||
|
epformat=EpisodeFormat(offset="-1"),
|
||||||
|
background=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(state, errmsg)
|
||||||
|
# 手动集数偏移只能应用一次,避免 E14 + (-1) 被二次处理成 E12。
|
||||||
|
self.assertEqual([13], planned_episodes)
|
||||||
|
|
||||||
def test_completed_media_job_is_removed_after_last_meta_task_fails(self):
|
def test_completed_media_job_is_removed_after_last_meta_task_fails(self):
|
||||||
jobview = JobManager()
|
jobview = JobManager()
|
||||||
tasks = [make_task(episode) for episode in range(1, 4)]
|
tasks = [make_task(episode) for episode in range(1, 4)]
|
||||||
|
|||||||
Reference in New Issue
Block a user