From 9bcaed5dc19bdf6ae3484988678ca9f6043082c7 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 20 Sep 2026 13:59:49 +0800 Subject: [PATCH] fix(transfer): tolerate mtime precision drift in history gate --- app/application/history/retry.py | 16 ++++++++++++++-- app/chain/transfer/execution.py | 2 +- tests/test_transfer_history_gate.py | 22 ++++++++++++++++++++++ tests/test_transfer_tmdb_category.py | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/application/history/retry.py b/app/application/history/retry.py index 55c16d697..d7720125e 100644 --- a/app/application/history/retry.py +++ b/app/application/history/retry.py @@ -2,6 +2,7 @@ from __future__ import annotations +import math from typing import Any, Dict, Optional from app.application.configuration import TransferRetryConfig, get_transfer_retry_config @@ -17,6 +18,8 @@ from app.runtime.log import logger MIN_FAILED_RETRIES = 1 MAX_FAILED_RETRIES = 10 FAILED_RETRY_TTL = 24 * 3600 +# 不同存储后端对秒级修改时间的浮点和序列化误差可能达到毫秒;更大的变化仍视为新版本。 +MODIFY_TIME_TOLERANCE = 1e-3 # 缓存值会同时保存文件指纹,使同一路径的新版本获得独立重试预算。 _failed_retry_counts = TTLCache( @@ -140,14 +143,23 @@ def _is_file_version_changed( recorded_fingerprint: Dict[str, Any], current_fingerprint: Dict[str, Any], ) -> bool: - """判断两个可比文件指纹是否指向不同版本。""" + """判断两个可比文件指纹是否指向不同版本,忽略修改时间的毫秒级误差。""" for field in ("fileid", "modify_time", "size"): recorded_value = recorded_fingerprint.get(field) current_value = current_fingerprint.get(field) if ( recorded_value is not None and current_value is not None - and recorded_value != current_value + and ( + not math.isclose( + recorded_value, + current_value, + rel_tol=0.0, + abs_tol=MODIFY_TIME_TOLERANCE, + ) + if field == "modify_time" + else recorded_value != current_value + ) ): return True return False diff --git a/app/chain/transfer/execution.py b/app/chain/transfer/execution.py index 407fd027f..951591d7e 100644 --- a/app/chain/transfer/execution.py +++ b/app/chain/transfer/execution.py @@ -591,7 +591,7 @@ class TransferExecutionOwner(_TransferOwnerBase): if task.mediainfo.tmdb_id: error_message = "TMDB 信息未匹配到媒体分类,无法按媒体类别整理" else: - error_message = "未识别到 TMDB 辅助信息,无法按媒体类别整理" + error_message = "媒体识别结果未匹配到媒体分类,无法按媒体类别整理" logger.error(f"{task.fileitem.name} {error_message}") if task.preview: return False, error_message diff --git a/tests/test_transfer_history_gate.py b/tests/test_transfer_history_gate.py index 72532cb36..7925fee84 100644 --- a/tests/test_transfer_history_gate.py +++ b/tests/test_transfer_history_gate.py @@ -79,6 +79,28 @@ def test_evaluate_history_gate_skips_when_success_size_unchanged(): assert action == HistoryGateAction.SKIP +@pytest.mark.parametrize( + "current_modify_time, expected", + [ + (100.0005, HistoryGateAction.SKIP), + (100.01, HistoryGateAction.PASS_SIZE_CHANGED), + ], +) +def test_evaluate_history_gate_uses_tolerance_for_modify_time( + current_modify_time, expected, +): + """修改时间仅有毫秒级浮点漂移时应保持原版本,明显变化仍应放行。""" + history = make_history(status=True, size=1024, modify_time=100.0) + + action = evaluate_history_gate( + history, + file_size=1024, + file_modify_time=current_modify_time, + ) + + assert action == expected + + def test_evaluate_history_gate_skips_when_recorded_size_missing(): """成功记录缺少大小信息(如蓝光目录)时无法比对,保守跳过。""" history = make_history(status=True, has_src_fileitem=False) diff --git a/tests/test_transfer_tmdb_category.py b/tests/test_transfer_tmdb_category.py index 8ca7900b1..a08d813dc 100644 --- a/tests/test_transfer_tmdb_category.py +++ b/tests/test_transfer_tmdb_category.py @@ -161,7 +161,7 @@ def test_transfer_stops_when_automatic_category_has_no_tmdb_result(monkeypatch) state, message = chain._TransferChain__handle_transfer(task) assert not state - assert message == "未识别到 TMDB 辅助信息,无法按媒体类别整理" + assert message == "媒体识别结果未匹配到媒体分类,无法按媒体类别整理" assert task.mediainfo.media_source == MediaSource.AniList assert task.mediainfo.media_id == "1234" assert task.plan_checkpoint is not None