mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 22:04:57 +08:00
fix(monitor): 避免下载临时文件触发未稳定告警 (#7215)
This commit is contained in:
committed by
GitHub
parent
e1b276bf9b
commit
bde74da51d
@@ -322,6 +322,9 @@ class LocalDirectoryWatcher:
|
||||
if change_type not in self._HANDLE_CHANGES:
|
||||
continue
|
||||
event_path = Path(path_str)
|
||||
# 下载临时文件会持续写入,须在稳定性排队前排除,避免正常下载触发超时告警。
|
||||
if self._is_download_temporary_file(event_path):
|
||||
continue
|
||||
event = self._build_event(change_type=change_type, event_path=event_path)
|
||||
if not event:
|
||||
# 「文件已消失」与「挂载抖动瞬时不可见」在此刻无法区分,静默丢弃就是
|
||||
@@ -353,6 +356,19 @@ class LocalDirectoryWatcher:
|
||||
except Exception as err:
|
||||
logger.error(f"处理本地目录监控事件失败: {path_str} - {err}")
|
||||
|
||||
@staticmethod
|
||||
def _is_download_temporary_file(event_path: Path) -> bool:
|
||||
"""
|
||||
按当前下载临时后缀配置过滤文件,沿用整理分发器的大小写不敏感匹配。
|
||||
|
||||
:param event_path: 待检查的监控路径
|
||||
:return: 文件后缀是否属于配置的下载临时后缀
|
||||
"""
|
||||
return bool(event_path.suffix) and event_path.suffix.casefold() in {
|
||||
extension.casefold()
|
||||
for extension in get_runtime_setting('DOWNLOAD_TMPEXT')
|
||||
}
|
||||
|
||||
def _queue_stability_event(self, change_type: Change, event_path: Path,
|
||||
file_size: int) -> None:
|
||||
"""
|
||||
@@ -409,6 +425,9 @@ class LocalDirectoryWatcher:
|
||||
if not item:
|
||||
continue
|
||||
event_path = item["path"]
|
||||
# 配置可能在文件入队后变化,告警前再次核对,避免旧条目继续误报。
|
||||
if self._is_download_temporary_file(event_path):
|
||||
continue
|
||||
if (
|
||||
not item.get("warning_sent")
|
||||
and now - item.get("first_seen", now) >= self.FILE_STABILITY_WARNING_SECONDS
|
||||
|
||||
@@ -6,6 +6,7 @@ import threading
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from watchfiles import Change
|
||||
|
||||
from app.application.history import TransferHistorySnapshot
|
||||
@@ -174,6 +175,130 @@ def test_running_watcher_reports_long_unstable_file_and_recovery(tmp_path, monke
|
||||
assert len(callback.events) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("suffix", [".!qB", ".!QB", ".part", ".PART", ".aria2"])
|
||||
def test_running_watcher_ignores_download_temporary_files(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, suffix: str
|
||||
) -> None:
|
||||
"""
|
||||
配置的临时下载后缀不进入稳定性队列,长期写入也不应告警。
|
||||
|
||||
:param tmp_path: 隔离的临时下载目录
|
||||
:param monkeypatch: 替换运行配置、监控线程状态和单调时钟
|
||||
:param suffix: 下载器使用的临时后缀,包含大小写和自定义后缀
|
||||
"""
|
||||
monkeypatch.setattr("app.monitor.watcher.get_runtime_setting", lambda _: [".!qb", ".part", ".aria2"])
|
||||
monkeypatch.setattr("app.monitor.watcher.time.monotonic", lambda: 1000.0)
|
||||
temporary_file = tmp_path / f"episode.mp4{suffix}"
|
||||
temporary_file.write_bytes(b"downloading")
|
||||
callback = CallbackRecorder()
|
||||
watcher = LocalDirectoryWatcher(tmp_path, callback=callback, force_polling=True)
|
||||
monkeypatch.setattr(watcher, "is_alive", lambda: True)
|
||||
|
||||
watcher._handle_changes({(Change.added, temporary_file.as_posix())})
|
||||
temporary_file.write_bytes(b"still downloading")
|
||||
watcher._handle_changes({(Change.modified, temporary_file.as_posix())})
|
||||
monkeypatch.setattr("app.monitor.watcher.time.monotonic", lambda: 2801.0)
|
||||
watcher._process_pending_stability()
|
||||
|
||||
assert watcher._pending_stability == {}
|
||||
assert callback.events == []
|
||||
assert callback.stability_events == []
|
||||
|
||||
|
||||
def test_running_watcher_dispatches_completed_download_after_rename(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""
|
||||
临时文件下载完成并改名后,正式视频仍需稳定两次再分发一次。
|
||||
|
||||
:param tmp_path: 隔离的临时下载目录
|
||||
:param monkeypatch: 替换临时后缀配置、监控线程状态和单调时钟
|
||||
"""
|
||||
monkeypatch.setattr("app.monitor.watcher.get_runtime_setting", lambda _: [".!qb"])
|
||||
monkeypatch.setattr("app.monitor.watcher.time.monotonic", lambda: 1000.0)
|
||||
temporary_file = tmp_path / "episode.mp4.!qB"
|
||||
completed_file = tmp_path / "episode.mp4"
|
||||
temporary_file.write_bytes(b"complete")
|
||||
callback = CallbackRecorder()
|
||||
watcher = LocalDirectoryWatcher(tmp_path, callback=callback, force_polling=True)
|
||||
monkeypatch.setattr(watcher, "is_alive", lambda: True)
|
||||
watcher._handle_changes({(Change.added, temporary_file.as_posix())})
|
||||
monkeypatch.setattr("app.monitor.watcher.time.monotonic", lambda: 2801.0)
|
||||
watcher._process_pending_stability()
|
||||
|
||||
temporary_file.rename(completed_file)
|
||||
watcher._handle_changes({
|
||||
(Change.deleted, temporary_file.as_posix()),
|
||||
(Change.added, completed_file.as_posix()),
|
||||
})
|
||||
monkeypatch.setattr("app.monitor.watcher.time.monotonic", lambda: 2803.0)
|
||||
watcher._process_pending_stability()
|
||||
assert callback.events == []
|
||||
monkeypatch.setattr("app.monitor.watcher.time.monotonic", lambda: 2805.0)
|
||||
watcher._process_pending_stability()
|
||||
|
||||
assert [event[2] for event in callback.events] == [completed_file.as_posix()]
|
||||
assert watcher._pending_stability == {}
|
||||
assert callback.stability_events == []
|
||||
|
||||
|
||||
def test_running_watcher_rechecks_temporary_suffix_for_pending_file(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""
|
||||
临时后缀配置热更新后,已入队的文件不应继续产生超时告警。
|
||||
|
||||
:param tmp_path: 隔离的临时下载目录
|
||||
:param monkeypatch: 替换运行配置、监控线程状态和单调时钟
|
||||
"""
|
||||
temporary_extensions: list[str] = []
|
||||
monkeypatch.setattr("app.monitor.watcher.get_runtime_setting", lambda _: temporary_extensions)
|
||||
monkeypatch.setattr("app.monitor.watcher.time.monotonic", lambda: 1000.0)
|
||||
temporary_file = tmp_path / "episode.mp4.!qB"
|
||||
temporary_file.write_bytes(b"downloading")
|
||||
callback = CallbackRecorder()
|
||||
watcher = LocalDirectoryWatcher(tmp_path, callback=callback, force_polling=True)
|
||||
monkeypatch.setattr(watcher, "is_alive", lambda: True)
|
||||
watcher._handle_changes({(Change.added, temporary_file.as_posix())})
|
||||
assert temporary_file.as_posix() in watcher._pending_stability
|
||||
|
||||
temporary_extensions.append(".!qb")
|
||||
monkeypatch.setattr("app.monitor.watcher.time.monotonic", lambda: 2801.0)
|
||||
watcher._process_pending_stability()
|
||||
|
||||
assert watcher._pending_stability == {}
|
||||
assert callback.events == []
|
||||
assert callback.stability_events == []
|
||||
|
||||
|
||||
def test_running_watcher_filters_temporary_files_in_added_directory(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""
|
||||
新增目录递归展开时跳过临时文件,并保留正式视频的稳定性检测。
|
||||
|
||||
:param tmp_path: 隔离的目录树
|
||||
:param monkeypatch: 替换监控运行配置和线程状态
|
||||
"""
|
||||
runtime_settings = {"DOWNLOAD_TMPEXT": [".!qb"], "MONITOR_RESCAN_DELAYS": "30,120"}
|
||||
monkeypatch.setattr("app.monitor.watcher.get_runtime_setting", runtime_settings.__getitem__)
|
||||
added_dir = tmp_path / "season"
|
||||
added_dir.mkdir()
|
||||
temporary_file = added_dir / "episode.mp4.!qB"
|
||||
completed_file = added_dir / "complete.mp4"
|
||||
temporary_file.write_bytes(b"downloading")
|
||||
completed_file.write_bytes(b"complete")
|
||||
callback = CallbackRecorder()
|
||||
watcher = LocalDirectoryWatcher(tmp_path, callback=callback, force_polling=True)
|
||||
monkeypatch.setattr(watcher, "is_alive", lambda: True)
|
||||
|
||||
watcher._handle_changes({(Change.added, added_dir.as_posix())})
|
||||
|
||||
assert set(watcher._pending_stability) == {completed_file.as_posix()}
|
||||
assert callback.events == []
|
||||
assert callback.stability_events == []
|
||||
|
||||
|
||||
def test_handle_changes_skips_missing_paths(tmp_path):
|
||||
"""
|
||||
事件到达时已经消失的路径不应触发整理。
|
||||
|
||||
Reference in New Issue
Block a user