mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
fix(config): separate auto-update and dev tracking settings for clarity
This commit is contained in:
@@ -292,7 +292,7 @@ class SystemUpdateManager(metaclass=SingletonClass):
|
|||||||
return min(100, int(downloaded_value * 100 / total_value))
|
return min(100, int(downloaded_value * 100 / total_value))
|
||||||
|
|
||||||
def get_status(self) -> SystemUpdateStatus:
|
def get_status(self) -> SystemUpdateStatus:
|
||||||
"""返回状态快照,并在新进程中收敛已完成的安装状态。"""
|
"""收敛安装状态并返回快照;提醒开关实时读取,避免缓存绕过关闭设置。"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
state = self._read_state()
|
state = self._read_state()
|
||||||
changed = False
|
changed = False
|
||||||
@@ -334,6 +334,8 @@ class SystemUpdateManager(metaclass=SingletonClass):
|
|||||||
state = self._persist_state(self._sync_aggregate(state))
|
state = self._persist_state(self._sync_aggregate(state))
|
||||||
else:
|
else:
|
||||||
state = self._sync_aggregate(state)
|
state = self._sync_aggregate(state)
|
||||||
|
state["auto_update"] = get_runtime_setting("MOVIEPILOT_AUTO_UPDATE") is True
|
||||||
|
state["auto_update_resource"] = get_runtime_setting("AUTO_UPDATE_RESOURCE") is True
|
||||||
return cast(SystemUpdateStatus, SystemUpdateStatus.model_validate(state))
|
return cast(SystemUpdateStatus, SystemUpdateStatus.model_validate(state))
|
||||||
|
|
||||||
def _is_install_applied(self, item: dict[str, Any], target: SystemUpdateType) -> bool:
|
def _is_install_applied(self, item: dict[str, Any], target: SystemUpdateType) -> bool:
|
||||||
@@ -374,6 +376,16 @@ class SystemUpdateManager(metaclass=SingletonClass):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def check_scheduled(self) -> SystemUpdateStatus:
|
||||||
|
"""按实时开关分别检查主程序和资源,避免热重载前的排队任务越过关闭设置。"""
|
||||||
|
for target, setting in (
|
||||||
|
(_APPLICATION, "MOVIEPILOT_AUTO_UPDATE"),
|
||||||
|
(_RESOURCES, "AUTO_UPDATE_RESOURCE"),
|
||||||
|
):
|
||||||
|
if get_runtime_setting(setting) is True:
|
||||||
|
self.check(target)
|
||||||
|
return self.get_status()
|
||||||
|
|
||||||
def check(self, target: SystemUpdateType | None = None) -> SystemUpdateStatus:
|
def check(self, target: SystemUpdateType | None = None) -> SystemUpdateStatus:
|
||||||
"""检查主程序和站点资源更新,定时检查失败只记录在对应明细中。"""
|
"""检查主程序和站点资源更新,定时检查失败只记录在对应明细中。"""
|
||||||
targets = (target,) if target else _TARGETS
|
targets = (target,) if target else _TARGETS
|
||||||
|
|||||||
@@ -179,6 +179,12 @@ class SchedulerRuntimeConfig:
|
|||||||
usage_statistic_share: bool
|
usage_statistic_share: bool
|
||||||
site_link: str | None
|
site_link: str | None
|
||||||
auto_update: bool = False
|
auto_update: bool = False
|
||||||
|
auto_update_resource: bool = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_check_enabled(self) -> bool:
|
||||||
|
"""主程序或资源任一检查开启时保留共享的定时检测服务。"""
|
||||||
|
return self.auto_update or self.auto_update_resource
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|||||||
+4
-2
@@ -243,9 +243,10 @@ def _git_current_branch() -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _auto_update_mode() -> str:
|
def _auto_update_mode() -> str:
|
||||||
|
"""启动时仅由独立 Dev 开关或一次性更新请求选择开发分支。"""
|
||||||
if SystemHelper.consume_one_shot_dev_update():
|
if SystemHelper.consume_one_shot_dev_update():
|
||||||
return "dev"
|
return "dev"
|
||||||
return str(get_runtime_setting("MOVIEPILOT_AUTO_UPDATE") or "").strip().lower()
|
return "dev" if get_runtime_setting("MOVIEPILOT_UPDATE_DEV") is True else "false"
|
||||||
|
|
||||||
|
|
||||||
def _file_sha256(path: Path) -> str:
|
def _file_sha256(path: Path) -> str:
|
||||||
@@ -490,6 +491,7 @@ def _resolve_auto_update_targets(mode: str) -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _best_effort_auto_update() -> None:
|
def _best_effort_auto_update() -> None:
|
||||||
|
"""优先应用已确认的安装包,再按 Dev 跟踪偏好更新;失败不阻断启动。"""
|
||||||
if _apply_prepared_release_update():
|
if _apply_prepared_release_update():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -521,7 +523,7 @@ def _best_effort_auto_update() -> None:
|
|||||||
str(get_runtime_setting("CONFIG_PATH")),
|
str(get_runtime_setting("CONFIG_PATH")),
|
||||||
]
|
]
|
||||||
|
|
||||||
click.echo(f"检测到 MOVIEPILOT_AUTO_UPDATE={mode},启动前执行本地自动更新")
|
click.echo("检测到 Dev 跟踪开关或一次性更新请求,启动前执行本地开发版更新")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
update_command,
|
update_command,
|
||||||
cwd=str(_repo_root()),
|
cwd=str(_repo_root()),
|
||||||
|
|||||||
+18
-18
@@ -335,8 +335,10 @@ class ConfigModel(BaseModel):
|
|||||||
ALIPAN_APP_ID: str = "ac1bf04dc9fd4d9aaabb65b4a668d403"
|
ALIPAN_APP_ID: str = "ac1bf04dc9fd4d9aaabb65b4a668d403"
|
||||||
|
|
||||||
# ==================== 系统升级配置 ====================
|
# ==================== 系统升级配置 ====================
|
||||||
# 开发版仍可在启动时跟踪 v3 分支;Release 更新由后台更新服务管理。
|
# 自动检查稳定版本并提示升级,不自动下载或安装。
|
||||||
MOVIEPILOT_AUTO_UPDATE: str = "false"
|
MOVIEPILOT_AUTO_UPDATE: bool = False
|
||||||
|
# 独立控制启动时跟踪 v3 开发分支。
|
||||||
|
MOVIEPILOT_UPDATE_DEV: bool = False
|
||||||
# 后台检查站点资源包,确认后由启动器在进程拉起前应用
|
# 后台检查站点资源包,确认后由启动器在进程拉起前应用
|
||||||
AUTO_UPDATE_RESOURCE: bool = True
|
AUTO_UPDATE_RESOURCE: bool = True
|
||||||
|
|
||||||
@@ -820,6 +822,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
|||||||
) -> Tuple[Any, bool]:
|
) -> Tuple[Any, bool]:
|
||||||
"""
|
"""
|
||||||
通用类型转换函数,根据预期类型转换值。如果转换失败,返回默认值
|
通用类型转换函数,根据预期类型转换值。如果转换失败,返回默认值
|
||||||
|
旧自动更新模式 dev/release 统一兼容为开启检查,运行时只保留布尔值。
|
||||||
:return: 元组 (转换后的值, 是否需要更新)
|
:return: 元组 (转换后的值, 是否需要更新)
|
||||||
"""
|
"""
|
||||||
if isinstance(value, (list, dict, set)):
|
if isinstance(value, (list, dict, set)):
|
||||||
@@ -830,6 +833,8 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
|||||||
|
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
|
if field_name == "MOVIEPILOT_AUTO_UPDATE" and value.lower() in {"dev", "release"}:
|
||||||
|
value = True
|
||||||
|
|
||||||
# 处理 Optional 类型:当值为空字符串且类型允许 None 时,转为 None
|
# 处理 Optional 类型:当值为空字符串且类型允许 None 时,转为 None
|
||||||
# 兼容 typing.Union (Python 3.9) 与 types.UnionType (Python 3.10+ PEP 604)
|
# 兼容 typing.Union (Python 3.9) 与 types.UnionType (Python 3.10+ PEP 604)
|
||||||
@@ -911,25 +916,18 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def generic_type_validator(cls, data: Any): # noqa
|
def generic_type_validator(cls, data: Any): # noqa
|
||||||
"""
|
"""
|
||||||
通用校验器,尝试将配置值转换为期望的类型
|
通用校验器,迁移旧 Dev 跟踪偏好后将配置值转换为期望的类型。
|
||||||
"""
|
"""
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# 仅 true 表示启用后台 Release 检查,其他模式不注册该定时服务。
|
# 新开关未配置时保留旧 Dev 跟踪偏好,显式设置的新开关始终优先。
|
||||||
if "MOVIEPILOT_AUTO_UPDATE" in data:
|
if (
|
||||||
original_update_mode = data["MOVIEPILOT_AUTO_UPDATE"]
|
str(data.get("MOVIEPILOT_AUTO_UPDATE", "")).strip().lower() == "dev"
|
||||||
mode = str(original_update_mode or "").strip().lower()
|
and "MOVIEPILOT_UPDATE_DEV" not in data
|
||||||
normalized_update_mode = (
|
):
|
||||||
mode if mode in {"true", "dev", "false"} else "false"
|
cls.update_env_config("MOVIEPILOT_UPDATE_DEV", None, True)
|
||||||
)
|
data["MOVIEPILOT_UPDATE_DEV"] = True
|
||||||
if normalized_update_mode != str(original_update_mode):
|
|
||||||
cls.update_env_config(
|
|
||||||
"MOVIEPILOT_AUTO_UPDATE",
|
|
||||||
original_update_mode,
|
|
||||||
normalized_update_mode,
|
|
||||||
)
|
|
||||||
data["MOVIEPILOT_AUTO_UPDATE"] = normalized_update_mode
|
|
||||||
|
|
||||||
# 处理 API_TOKEN 特殊验证
|
# 处理 API_TOKEN 特殊验证
|
||||||
if "API_TOKEN" in data:
|
if "API_TOKEN" in data:
|
||||||
@@ -964,7 +962,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
|||||||
field_name: str, original_value: Any, converted_value: Any
|
field_name: str, original_value: Any, converted_value: Any
|
||||||
) -> Tuple[bool, str]:
|
) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
更新 env 配置
|
更新 env 配置;版本更新开关以小写 true/false 持久化,供启动脚本读取。
|
||||||
"""
|
"""
|
||||||
# 成功且无提示时使用空字符串,保证与 Tuple[bool, str] 返回类型一致
|
# 成功且无提示时使用空字符串,保证与 Tuple[bool, str] 返回类型一致
|
||||||
message = ""
|
message = ""
|
||||||
@@ -993,6 +991,8 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
|||||||
# 如果是列表、字典或集合类型,将其转换为JSON字符串
|
# 如果是列表、字典或集合类型,将其转换为JSON字符串
|
||||||
if isinstance(converted_value, (list, dict, set)):
|
if isinstance(converted_value, (list, dict, set)):
|
||||||
value_to_write = json.dumps(converted_value)
|
value_to_write = json.dumps(converted_value)
|
||||||
|
elif field_name in {"MOVIEPILOT_AUTO_UPDATE", "MOVIEPILOT_UPDATE_DEV"}:
|
||||||
|
value_to_write = str(converted_value).lower()
|
||||||
else:
|
else:
|
||||||
value_to_write = str(converted_value)
|
value_to_write = str(converted_value)
|
||||||
|
|
||||||
|
|||||||
@@ -204,8 +204,8 @@ class SchedulerCatalogOwner(_SchedulerOwnerBase):
|
|||||||
JobSpec("agent_heartbeat", "智能体定时任务", self.agent_heartbeat, "agent"),
|
JobSpec("agent_heartbeat", "智能体定时任务", self.agent_heartbeat, "agent"),
|
||||||
JobSpec("usage_report", "安装版本统计上报", MoviePilotServerHelper.report_usage, "server"),
|
JobSpec("usage_report", "安装版本统计上报", MoviePilotServerHelper.report_usage, "server"),
|
||||||
*(
|
*(
|
||||||
[JobSpec("system_update_check", "检查系统更新", system_update_manager.check, "system")]
|
[JobSpec("system_update_check", "检查系统更新", system_update_manager.check_scheduled, "system")]
|
||||||
if config.auto_update
|
if config.update_check_enabled
|
||||||
else []
|
else []
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -428,8 +428,8 @@ class SchedulerCatalogOwner(_SchedulerOwnerBase):
|
|||||||
kwargs={"job_id": "plugin_market_refresh"},
|
kwargs={"job_id": "plugin_market_refresh"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.auto_update:
|
if config.update_check_enabled:
|
||||||
# 更新检查只缓存 Release 元数据,不会在未授权时下载或重启。
|
# 任一更新开关开启即注册,执行时分别检查已启用的主程序或资源。
|
||||||
self._scheduler.add_job(
|
self._scheduler.add_job(
|
||||||
self.start,
|
self.start,
|
||||||
"interval",
|
"interval",
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class Scheduler(
|
|||||||
"DB_BACKUP_CRON",
|
"DB_BACKUP_CRON",
|
||||||
"USAGE_STATISTIC_SHARE",
|
"USAGE_STATISTIC_SHARE",
|
||||||
"MOVIEPILOT_AUTO_UPDATE",
|
"MOVIEPILOT_AUTO_UPDATE",
|
||||||
|
"AUTO_UPDATE_RESOURCE",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|||||||
@@ -253,6 +253,9 @@ class SystemUpdateRequest(BaseModel): # type: ignore[misc]
|
|||||||
class SystemUpdateStatus(BaseModel):
|
class SystemUpdateStatus(BaseModel):
|
||||||
"""主程序与站点资源后台更新的聚合状态快照。"""
|
"""主程序与站点资源后台更新的聚合状态快照。"""
|
||||||
|
|
||||||
|
auto_update: bool = Field(default=False, description="是否启用主程序自动检查及升级提醒")
|
||||||
|
auto_update_resource: bool = Field(default=True, description="是否启用站点资源自动检查及升级提醒")
|
||||||
|
|
||||||
state: Literal[
|
state: Literal[
|
||||||
"idle",
|
"idle",
|
||||||
"available",
|
"available",
|
||||||
|
|||||||
@@ -189,7 +189,8 @@ def build_scheduler_runtime_config(settings: Settings) -> SchedulerRuntimeConfig
|
|||||||
ai_agent_job_interval=settings.AI_AGENT_JOB_INTERVAL,
|
ai_agent_job_interval=settings.AI_AGENT_JOB_INTERVAL,
|
||||||
usage_statistic_share=settings.USAGE_STATISTIC_SHARE,
|
usage_statistic_share=settings.USAGE_STATISTIC_SHARE,
|
||||||
site_link=settings.MP_DOMAIN("#/site"),
|
site_link=settings.MP_DOMAIN("#/site"),
|
||||||
auto_update=str(settings.MOVIEPILOT_AUTO_UPDATE).strip().lower() == "true",
|
auto_update=settings.MOVIEPILOT_AUTO_UPDATE,
|
||||||
|
auto_update_resource=settings.AUTO_UPDATE_RESOURCE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function apply_package_cache_env() {
|
|||||||
# 优先级: 系统环境变量 -> .env 文件 (即使为空字符串) -> 预设默认值
|
# 优先级: 系统环境变量 -> .env 文件 (即使为空字符串) -> 预设默认值
|
||||||
# 精准适配 Python 端 set_key (quote_mode="always", 单引号包裹, \' 转义)
|
# 精准适配 Python 端 set_key (quote_mode="always", 单引号包裹, \' 转义)
|
||||||
function load_config_from_app_env() {
|
function load_config_from_app_env() {
|
||||||
|
# 保留未配置的新 Dev 开关为空,交由更新器兼容旧模式、Python 持久化迁移。
|
||||||
|
|
||||||
local env_file="${CONFIG_DIR}/app.env"
|
local env_file="${CONFIG_DIR}/app.env"
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ function load_config_from_app_env() {
|
|||||||
["PROXY_HOST"]=""
|
["PROXY_HOST"]=""
|
||||||
["GITHUB_TOKEN"]=""
|
["GITHUB_TOKEN"]=""
|
||||||
["MOVIEPILOT_AUTO_UPDATE"]="false"
|
["MOVIEPILOT_AUTO_UPDATE"]="false"
|
||||||
|
["MOVIEPILOT_UPDATE_DEV"]=""
|
||||||
["MOVIEPILOT_FORCE_CHOWN"]="false"
|
["MOVIEPILOT_FORCE_CHOWN"]="false"
|
||||||
["MOVIEPILOT_SAFE_MODE"]="false"
|
["MOVIEPILOT_SAFE_MODE"]="false"
|
||||||
["BROWSER_EMULATION"]="cloakbrowser"
|
["BROWSER_EMULATION"]="cloakbrowser"
|
||||||
@@ -323,10 +325,10 @@ function run_pending_dev_update_after_supervisor_shutdown() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local update_exit_code=0
|
local update_exit_code=0
|
||||||
MOVIEPILOT_AUTO_UPDATE="dev"
|
MOVIEPILOT_UPDATE_DEV="true"
|
||||||
INFO "检测到受管重启的 Dev 更新请求"
|
INFO "检测到受管重启的 Dev 更新请求"
|
||||||
run_moviepilot_update || update_exit_code=$?
|
run_moviepilot_update || update_exit_code=$?
|
||||||
MOVIEPILOT_AUTO_UPDATE="${MOVIEPILOT_AUTO_UPDATE_ORIGINAL}"
|
MOVIEPILOT_UPDATE_DEV="${MOVIEPILOT_UPDATE_DEV_ORIGINAL}"
|
||||||
|
|
||||||
[ "${update_exit_code}" -eq 0 ] \
|
[ "${update_exit_code}" -eq 0 ] \
|
||||||
&& [ "${MOVIEPILOT_UPDATE_RESULT:-noop}" = "updated" ]
|
&& [ "${MOVIEPILOT_UPDATE_RESULT:-noop}" = "updated" ]
|
||||||
@@ -460,10 +462,10 @@ apply_package_cache_env
|
|||||||
ONE_SHOT_DEV_UPDATE_FLAG="${CONFIG_DIR}/temp/moviepilot.pending_dev_update"
|
ONE_SHOT_DEV_UPDATE_FLAG="${CONFIG_DIR}/temp/moviepilot.pending_dev_update"
|
||||||
SUPERVISOR_RESTART_REQUEST_FILE="${CONFIG_DIR}/temp/moviepilot.pending_supervisor_restart"
|
SUPERVISOR_RESTART_REQUEST_FILE="${CONFIG_DIR}/temp/moviepilot.pending_supervisor_restart"
|
||||||
ONE_SHOT_DEV_UPDATE="false"
|
ONE_SHOT_DEV_UPDATE="false"
|
||||||
MOVIEPILOT_AUTO_UPDATE_ORIGINAL="${MOVIEPILOT_AUTO_UPDATE}"
|
MOVIEPILOT_UPDATE_DEV_ORIGINAL="${MOVIEPILOT_UPDATE_DEV}"
|
||||||
if [ -f "${ONE_SHOT_DEV_UPDATE_FLAG}" ]; then
|
if [ -f "${ONE_SHOT_DEV_UPDATE_FLAG}" ]; then
|
||||||
rm -f "${ONE_SHOT_DEV_UPDATE_FLAG}"
|
rm -f "${ONE_SHOT_DEV_UPDATE_FLAG}"
|
||||||
MOVIEPILOT_AUTO_UPDATE="dev"
|
MOVIEPILOT_UPDATE_DEV="true"
|
||||||
ONE_SHOT_DEV_UPDATE="true"
|
ONE_SHOT_DEV_UPDATE="true"
|
||||||
INFO "检测到一次性 Dev 更新标记,本次启动将更新开发分支"
|
INFO "检测到一次性 Dev 更新标记,本次启动将更新开发分支"
|
||||||
fi
|
fi
|
||||||
@@ -489,7 +491,7 @@ else
|
|||||||
MOVIEPILOT_UPDATE_RESULT="noop"
|
MOVIEPILOT_UPDATE_RESULT="noop"
|
||||||
fi
|
fi
|
||||||
if [ "${ONE_SHOT_DEV_UPDATE}" = "true" ]; then
|
if [ "${ONE_SHOT_DEV_UPDATE}" = "true" ]; then
|
||||||
MOVIEPILOT_AUTO_UPDATE="${MOVIEPILOT_AUTO_UPDATE_ORIGINAL}"
|
MOVIEPILOT_UPDATE_DEV="${MOVIEPILOT_UPDATE_DEV_ORIGINAL}"
|
||||||
fi
|
fi
|
||||||
if [ "${UPDATE_RECOVERY_REQUIRED:-false}" = "true" ]; then
|
if [ "${UPDATE_RECOVERY_REQUIRED:-false}" = "true" ]; then
|
||||||
ERROR "→ 容器更新回滚未完成,停止启动。"
|
ERROR "→ 容器更新回滚未完成,停止启动。"
|
||||||
|
|||||||
+6
-1
@@ -595,8 +595,13 @@ function configure_package_route() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function run_moviepilot_update() {
|
function run_moviepilot_update() {
|
||||||
|
# 新 Dev 开关独立于自动检查;仅在未配置新开关时兼容首次启动的旧 dev 值。
|
||||||
MOVIEPILOT_UPDATE_RESULT="noop"
|
MOVIEPILOT_UPDATE_RESULT="noop"
|
||||||
if [ "${MOVIEPILOT_AUTO_UPDATE}" = "dev" ]; then
|
local dev_update="${MOVIEPILOT_UPDATE_DEV:-}"
|
||||||
|
if [ -z "${dev_update}" ] && [[ "${MOVIEPILOT_AUTO_UPDATE:-}" == [Dd][Ee][Vv] ]]; then
|
||||||
|
dev_update="true"
|
||||||
|
fi
|
||||||
|
if [[ "${dev_update}" == [Tt][Rr][Uu][Ee] ]]; then
|
||||||
TMP_PATH=$(mktemp -d)
|
TMP_PATH=$(mktemp -d)
|
||||||
if [ ! -d "${TMP_PATH}" ]; then
|
if [ ! -d "${TMP_PATH}" ]; then
|
||||||
TMP_PATH=/tmp/mp_update_path
|
TMP_PATH=/tmp/mp_update_path
|
||||||
|
|||||||
+1
-1
@@ -393,7 +393,7 @@ moviepilot version
|
|||||||
|
|
||||||
- `start` 会先启动后端,再启动前端
|
- `start` 会先启动后端,再启动前端
|
||||||
- `start --safe` 会以安全模式启动后端,本次启动跳过插件、调度器、监控、命令和工作流等后台扩展能力,不修改用户配置
|
- `start --safe` 会以安全模式启动后端,本次启动跳过插件、调度器、监控、命令和工作流等后台扩展能力,不修改用户配置
|
||||||
- `MOVIEPILOT_AUTO_UPDATE` 默认关闭;设置为 `true` 时启用后台 Release 检查,设置为 `dev` 时保留启动前跟踪当前 v3 开发分支的行为,更新失败只告警,不阻断当前启动
|
- `MOVIEPILOT_AUTO_UPDATE` 为布尔开关,默认 `false`;只有 `true` 启用后台 Release 检查和版本提醒,保存后定时服务热更新。`AUTO_UPDATE_RESOURCE` 独立控制站点资源检查和提醒;任一开关开启即启用检测服务,且只检查对应目标,两者均关闭才移除服务。`MOVIEPILOT_UPDATE_DEV` 为独立布尔开关,默认 `false`;设为 `true` 时在每次启动/重启前跟踪当前 v3 开发分支,更新失败只告警,不阻断当前启动。旧 `dev/release` 值统一转换为 `MOVIEPILOT_AUTO_UPDATE=true`;旧 `dev` 在未显式配置新开关时迁移为 `MOVIEPILOT_UPDATE_DEV=true`
|
||||||
- Release 更新由后台每 6 小时检查 GitHub Release;管理员确认后先静默下载安装包并显示进度,下载完成后再次确认重启,启动阶段只安装已下载且通过 SHA-256 校验的包
|
- Release 更新由后台每 6 小时检查 GitHub Release;管理员确认后先静默下载安装包并显示进度,下载完成后再次确认重启,启动阶段只安装已下载且通过 SHA-256 校验的包
|
||||||
- 页面中的“稍后”会在当前浏览器暂停提醒 24 小时,“忽略此版本”只屏蔽当前版本;出现更高版本时会重新提示
|
- 页面中的“稍后”会在当前浏览器暂停提醒 24 小时,“忽略此版本”只屏蔽当前版本;出现更高版本时会重新提示
|
||||||
- 通过系统内置的重启入口触发重启时,本地 CLI 安装模式也会复用同一套前后端进程管理完成重启
|
- 通过系统内置的重启入口触发重启时,本地 CLI 安装模式也会复用同一套前后端进程管理完成重启
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ source 其他脚本,可能出现同一次启动混用新旧脚本的情况。
|
|||||||
/config/temp/moviepilot.pending_dev_update
|
/config/temp/moviepilot.pending_dev_update
|
||||||
```
|
```
|
||||||
|
|
||||||
entrypoint 会删除该标记,并只在本次启动中把 `MOVIEPILOT_AUTO_UPDATE` 临时设为 `dev`。更新阶段结束后
|
entrypoint 会删除该标记,并只在本次启动中把 `MOVIEPILOT_UPDATE_DEV` 临时设为 `true`。更新阶段结束后
|
||||||
恢复原值,避免把一次性操作变成永久自动更新。
|
恢复原值,避免把一次性操作变成永久自动更新。
|
||||||
|
|
||||||
### 5.2 未完成更新恢复
|
### 5.2 未完成更新恢复
|
||||||
@@ -213,7 +213,7 @@ Alembic migration。保留当前载荷并恢复其依赖,可以避免形成“
|
|||||||
|
|
||||||
### 5.4 Dev 自动更新
|
### 5.4 Dev 自动更新
|
||||||
|
|
||||||
仅当 `MOVIEPILOT_AUTO_UPDATE=dev` 时,启动脚本会联网获取 `v3` 分支源码和最新 V3 前端 Release。
|
仅当 `MOVIEPILOT_UPDATE_DEV=true` 时(首次升级兼容尚未迁移且未配置新开关的旧 `MOVIEPILOT_AUTO_UPDATE=dev`),启动脚本会联网获取 `v3` 分支源码和最新 V3 前端 Release。
|
||||||
GitHub 访问按 `GITHUB_PROXY`、`PROXY_HOST`、直连顺序选择;包索引按 `PIP_PROXY`、`PROXY_HOST`、
|
GitHub 访问按 `GITHUB_PROXY`、`PROXY_HOST`、直连顺序选择;包索引按 `PIP_PROXY`、`PROXY_HOST`、
|
||||||
直连顺序选择。
|
直连顺序选择。
|
||||||
|
|
||||||
@@ -458,7 +458,8 @@ Docker restart policy。
|
|||||||
| `UMASK` | `000` | 后端进程文件权限掩码。 |
|
| `UMASK` | `000` | 后端进程文件权限掩码。 |
|
||||||
| `PORT` | `3001` | 后端监听和 readiness 端口。 |
|
| `PORT` | `3001` | 后端监听和 readiness 端口。 |
|
||||||
| `NGINX_PORT` | `3000` | HTTP 前端入口。 |
|
| `NGINX_PORT` | `3000` | HTTP 前端入口。 |
|
||||||
| `MOVIEPILOT_AUTO_UPDATE` | `false` | 只有 `dev` 会触发 `update.sh` 的启动时分支更新;稳定版由后台下载和 root worker 安装。 |
|
| `MOVIEPILOT_AUTO_UPDATE` | `false` | 布尔开关,仅 `true` 开启后台版本检查和升级提醒;关闭后不检查主程序;`AUTO_UPDATE_RESOURCE=true` 时仍启用服务且只检查站点资源。稳定版下载及安装需手动确认。旧 `dev/release` 统一迁移为 `true`。 |
|
||||||
|
| `MOVIEPILOT_UPDATE_DEV` | `false` | 独立布尔开关,`true` 触发 `update.sh` 的启动时 Dev 分支更新;旧 `dev` 在未显式配置此开关时保留跟踪偏好。 |
|
||||||
| `MOVIEPILOT_SAFE_MODE` | `false` | 跳过普通模式专属的插件及后台控制面。 |
|
| `MOVIEPILOT_SAFE_MODE` | `false` | 跳过普通模式专属的插件及后台控制面。 |
|
||||||
| `MOVIEPILOT_FORCE_CHOWN` | `false` | 是否执行大范围递归权限修复。 |
|
| `MOVIEPILOT_FORCE_CHOWN` | `false` | 是否执行大范围递归权限修复。 |
|
||||||
| `PACKAGE_CACHE_ROOT` | `/config/.cache` | 包管理缓存根目录。 |
|
| `PACKAGE_CACHE_ROOT` | `/config/.cache` | 包管理缓存根目录。 |
|
||||||
|
|||||||
+2
-2
@@ -253,11 +253,11 @@ FastAPI 的 HTTP 异常和参数校验异常统一使用 `message`,不再返
|
|||||||
|
|
||||||
#### 系统更新
|
#### 系统更新
|
||||||
|
|
||||||
系统 Release 更新采用“检查、后台下载、确认安装”三阶段流程,以下接口均要求超级管理员登录态。后台每 6 小时自动检查一次稳定版 v3 GitHub Release 和站点资源包;升级类型只有 `application`(主程序,前端版本由后端 Release 中的 `version.py` 决定)与 `resources`(认证资源和索引资源)。下载完成前不重启服务,安装接口只消费已下载并校验的完整制品,启动器会先应用主程序包,再应用资源包,之后才启动进程;启动后的初始化不会再次下载或触发资源重启。原 Dev 更新入口继续保留,但 `/system/upgrade` 只接受请求体 `"dev"`,不再处理 Release 更新。
|
系统 Release 更新采用“检查、后台下载、确认安装”三阶段流程,以下接口均要求超级管理员登录态。后台每 6 小时按独立开关检查更新:`MOVIEPILOT_AUTO_UPDATE=true` 检查稳定版 v3 GitHub Release,`AUTO_UPDATE_RESOURCE=true` 检查站点资源包,并分别提示升级。任一开关开启即启用定时服务;两者均关闭时移除定时服务并隐藏版本提醒。手动检查、下载和安装仍可用。独立布尔配置 `MOVIEPILOT_UPDATE_DEV` 控制启动时跟踪 Dev 分支;升级类型只有 `application`(主程序,前端版本由后端 Release 中的 `version.py` 决定)与 `resources`(认证资源和索引资源)。下载完成前不重启服务,安装接口只消费已下载并校验的完整制品,启动器会先应用主程序包,再应用资源包,之后才启动进程;启动后的初始化不会再次下载或触发资源重启。原 Dev 更新入口继续保留,但 `/system/upgrade` 只接受请求体 `"dev"`,不再处理 Release 更新。
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| GET | `/api/v1/system/update/status` | 查询聚合状态及 `updates` 中两类升级明细的 `idle`、`available`、`downloading`、`ready`、`installing` 或 `failed` 状态,以及版本、字节数和进度 |
|
| GET | `/api/v1/system/update/status` | 查询聚合状态、实时提醒开关 `auto_update` / `auto_update_resource` 及 `updates` 中两类升级明细的 `idle`、`available`、`downloading`、`ready`、`installing` 或 `failed` 状态,以及版本、字节数和进度 |
|
||||||
| POST | `/api/v1/system/update/check` | 立即检查最新稳定版 v3 Release 和当前平台站点资源包 |
|
| POST | `/api/v1/system/update/check` | 立即检查最新稳定版 v3 Release 和当前平台站点资源包 |
|
||||||
| POST | `/api/v1/system/update/download` | 请求体可传 `{"target":"application"}` 或 `{"target":"resources"}`;后台下载并校验对应制品 |
|
| POST | `/api/v1/system/update/download` | 请求体可传 `{"target":"application"}` 或 `{"target":"resources"}`;后台下载并校验对应制品 |
|
||||||
| POST | `/api/v1/system/update/install` | 请求体可传 `{"target":"application"}` 或 `{"target":"resources"}`;再次校验对应制品,写入安装意图并重启 |
|
| POST | `/api/v1/system/update/install` | 请求体可传 `{"target":"application"}` 或 `{"target":"resources"}`;再次校验对应制品,写入安装意图并重启 |
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ moviepilot update all --ref latest --frontend-version latest
|
|||||||
moviepilot update all --skip-resources
|
moviepilot update all --skip-resources
|
||||||
```
|
```
|
||||||
|
|
||||||
`MOVIEPILOT_AUTO_UPDATE` defaults to `false`. Setting it to `true` enables the background Release check; setting it to `dev` retains branch-tracking updates during `start/restart`. The setting is hot-reloaded by the scheduler.
|
`MOVIEPILOT_AUTO_UPDATE` is a boolean (default `false`): `true` enables the background Release check and version reminders, and `false` disables application checks and reminders. `AUTO_UPDATE_RESOURCE` independently enables resource checks and reminders; the scheduled service exists when either switch is enabled and checks only enabled targets. The scheduler hot-reloads this switch. `MOVIEPILOT_UPDATE_DEV` is an independent boolean (default `false`) that enables development-branch updates during `start/restart`. Legacy `dev`/`release` values of `MOVIEPILOT_AUTO_UPDATE` normalize to `true`; legacy `dev` also preserves Dev tracking when the new switch is not explicitly configured.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from pathlib import Path
|
|||||||
from types import ModuleType, SimpleNamespace
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
MODULE_PATH = Path(__file__).resolve().parents[1] / "app" / "cli.py"
|
MODULE_PATH = Path(__file__).resolve().parents[1] / "app" / "cli.py"
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ class _DummySystemHelper:
|
|||||||
|
|
||||||
|
|
||||||
def load_cli_module():
|
def load_cli_module():
|
||||||
|
"""隔离加载 CLI,使用真实布尔配置形状验证启动更新决策。"""
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
root = Path(temp_dir)
|
root = Path(temp_dir)
|
||||||
settings = SimpleNamespace(
|
settings = SimpleNamespace(
|
||||||
@@ -35,7 +38,8 @@ def load_cli_module():
|
|||||||
PROXY_HOST="",
|
PROXY_HOST="",
|
||||||
PIP_PROXY="",
|
PIP_PROXY="",
|
||||||
GITHUB_TOKEN="",
|
GITHUB_TOKEN="",
|
||||||
MOVIEPILOT_AUTO_UPDATE="false",
|
MOVIEPILOT_AUTO_UPDATE=False,
|
||||||
|
MOVIEPILOT_UPDATE_DEV=False,
|
||||||
PROXY={},
|
PROXY={},
|
||||||
REPO_GITHUB_HEADERS=lambda _repo: {},
|
REPO_GITHUB_HEADERS=lambda _repo: {},
|
||||||
)
|
)
|
||||||
@@ -95,8 +99,9 @@ def test_resolve_auto_update_targets_keeps_dev_branch_tracking():
|
|||||||
|
|
||||||
|
|
||||||
def test_one_shot_dev_update_overrides_disabled_default():
|
def test_one_shot_dev_update_overrides_disabled_default():
|
||||||
|
"""一次性手动更新不受两个自动开关关闭的影响。"""
|
||||||
module = load_cli_module()
|
module = load_cli_module()
|
||||||
module.settings.MOVIEPILOT_AUTO_UPDATE = "false"
|
module.settings.MOVIEPILOT_AUTO_UPDATE = False
|
||||||
|
|
||||||
with patch.object(
|
with patch.object(
|
||||||
module.SystemHelper, "consume_one_shot_dev_update", return_value=True
|
module.SystemHelper, "consume_one_shot_dev_update", return_value=True
|
||||||
@@ -104,6 +109,16 @@ def test_one_shot_dev_update_overrides_disabled_default():
|
|||||||
assert module._auto_update_mode() == "dev"
|
assert module._auto_update_mode() == "dev"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("auto_update", [True, False])
|
||||||
|
@pytest.mark.parametrize("update_dev", [True, False])
|
||||||
|
def test_dev_tracking_is_independent_of_automatic_checks(auto_update, update_dev):
|
||||||
|
"""检查开关不触发启动更新,Dev 开关单独选择开发分支。"""
|
||||||
|
module = load_cli_module()
|
||||||
|
module.settings.MOVIEPILOT_AUTO_UPDATE = auto_update
|
||||||
|
module.settings.MOVIEPILOT_UPDATE_DEV = update_dev
|
||||||
|
assert module._auto_update_mode() == ("dev" if update_dev else "false")
|
||||||
|
|
||||||
|
|
||||||
def test_release_mode_does_not_update_during_start():
|
def test_release_mode_does_not_update_during_start():
|
||||||
module = load_cli_module()
|
module = load_cli_module()
|
||||||
with patch.object(module, "_auto_update_mode", return_value="release"), patch.object(
|
with patch.object(module, "_auto_update_mode", return_value="release"), patch.object(
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from dataclasses import replace
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from app.application.configuration import SchedulerRuntimeConfig
|
from app.application.configuration import SchedulerRuntimeConfig
|
||||||
from app.scheduler import catalog as scheduler_catalog
|
from app.scheduler import catalog as scheduler_catalog
|
||||||
from app.scheduler import maintenance as scheduler_maintenance
|
from app.scheduler import maintenance as scheduler_maintenance
|
||||||
@@ -58,6 +60,7 @@ def _config(**changes) -> SchedulerRuntimeConfig:
|
|||||||
usage_statistic_share=False,
|
usage_statistic_share=False,
|
||||||
site_link=None,
|
site_link=None,
|
||||||
auto_update=False,
|
auto_update=False,
|
||||||
|
auto_update_resource=False,
|
||||||
)
|
)
|
||||||
return replace(config, **changes)
|
return replace(config, **changes)
|
||||||
|
|
||||||
@@ -74,8 +77,9 @@ def test_database_backup_schedule_only_watches_job_shape() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_auto_update_setting_is_hot_reloadable() -> None:
|
def test_auto_update_setting_is_hot_reloadable() -> None:
|
||||||
"""自动更新开关变更时应触发 Scheduler 重建。"""
|
"""主程序或资源开关变更时均应触发 Scheduler 重建。"""
|
||||||
assert "MOVIEPILOT_AUTO_UPDATE" in Scheduler.CONFIG_WATCH
|
assert "MOVIEPILOT_AUTO_UPDATE" in Scheduler.CONFIG_WATCH
|
||||||
|
assert "AUTO_UPDATE_RESOURCE" in Scheduler.CONFIG_WATCH
|
||||||
|
|
||||||
|
|
||||||
def test_disabled_database_backup_does_not_register_job() -> None:
|
def test_disabled_database_backup_does_not_register_job() -> None:
|
||||||
@@ -108,8 +112,12 @@ def test_enabled_database_backup_registers_single_replaceable_job(monkeypatch) -
|
|||||||
assert scheduler._scheduler.jobs["database_backup"]["replace_existing"] is True
|
assert scheduler._scheduler.jobs["database_backup"]["replace_existing"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_auto_update_check_is_registered_only_when_enabled(monkeypatch) -> None:
|
@pytest.mark.parametrize("auto_update", [False, True])
|
||||||
"""只有显式开启自动更新时才注册 Release 检查任务。"""
|
@pytest.mark.parametrize("auto_update_resource", [False, True])
|
||||||
|
def test_auto_update_check_is_registered_only_when_enabled(
|
||||||
|
monkeypatch, auto_update, auto_update_resource
|
||||||
|
) -> None:
|
||||||
|
"""任一开关开启即注册检查任务,均关闭则不注册。"""
|
||||||
scheduler = _scheduler()
|
scheduler = _scheduler()
|
||||||
scheduler._services = Mock()
|
scheduler._services = Mock()
|
||||||
background_scheduler = Mock()
|
background_scheduler = Mock()
|
||||||
@@ -120,20 +128,14 @@ def test_auto_update_check_is_registered_only_when_enabled(monkeypatch) -> None:
|
|||||||
monkeypatch.setattr(scheduler, "init_agent_task_jobs", lambda: None)
|
monkeypatch.setattr(scheduler, "init_agent_task_jobs", lambda: None)
|
||||||
monkeypatch.setattr(scheduler, "init_plugin_jobs", lambda: None)
|
monkeypatch.setattr(scheduler, "init_plugin_jobs", lambda: None)
|
||||||
|
|
||||||
scheduler_catalog.SchedulerCatalogOwner._initialize_catalog(scheduler, _config(auto_update=False))
|
scheduler_catalog.SchedulerCatalogOwner._initialize_catalog(
|
||||||
assert not any(
|
scheduler, _config(auto_update=auto_update, auto_update_resource=auto_update_resource)
|
||||||
call.kwargs.get("id") == "system_update_check"
|
|
||||||
for call in background_scheduler.add_job.call_args_list
|
|
||||||
)
|
)
|
||||||
assert "system_update_check" not in scheduler._jobs
|
|
||||||
|
|
||||||
background_scheduler.add_job.reset_mock()
|
|
||||||
scheduler_catalog.SchedulerCatalogOwner._initialize_catalog(scheduler, _config(auto_update=True))
|
|
||||||
assert any(
|
assert any(
|
||||||
call.kwargs.get("id") == "system_update_check"
|
call.kwargs.get("id") == "system_update_check"
|
||||||
for call in background_scheduler.add_job.call_args_list
|
for call in background_scheduler.add_job.call_args_list
|
||||||
)
|
) is (auto_update or auto_update_resource)
|
||||||
assert "system_update_check" in scheduler._jobs
|
assert ("system_update_check" in scheduler._jobs) is (auto_update or auto_update_resource)
|
||||||
|
|
||||||
|
|
||||||
def test_scheduled_backup_uses_registered_database_governance(monkeypatch) -> None:
|
def test_scheduled_backup_uses_registered_database_governance(monkeypatch) -> None:
|
||||||
|
|||||||
@@ -794,17 +794,27 @@ def test_updater_package_proxy_stays_command_scoped(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("mode", "install_result", "expected"),
|
("mode", "dev_update", "install_result", "expected"),
|
||||||
(("false", "unused", "noop"), ("dev", "success", "updated"), ("dev", "failure", "failed")),
|
(
|
||||||
|
("false", "false", "unused", "noop"),
|
||||||
|
("true", "false", "unused", "noop"),
|
||||||
|
("false", "true", "success", "updated"),
|
||||||
|
("true", "True", "failure", "failed"),
|
||||||
|
("dev", "", "success", "updated"),
|
||||||
|
("dev", "false", "unused", "noop"),
|
||||||
|
("release", "", "unused", "noop"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
def test_updater_exposes_explicit_result(
|
def test_updater_exposes_explicit_result(
|
||||||
tmp_path: Path, mode: str, install_result: str, expected: str
|
tmp_path: Path, mode: str, dev_update: str, install_result: str, expected: str
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Docker 由独立 Dev 开关决定启动更新,并兼容首次迁移的旧模式。"""
|
||||||
script = textwrap.dedent(
|
script = textwrap.dedent(
|
||||||
f"""\
|
f"""\
|
||||||
CONFIG_DIR="$1"
|
CONFIG_DIR="$1"
|
||||||
MOVIEPILOT_AUTO_UPDATE="$2"
|
MOVIEPILOT_AUTO_UPDATE="$2"
|
||||||
INSTALL_RESULT="$3"
|
INSTALL_RESULT="$3"
|
||||||
|
MOVIEPILOT_UPDATE_DEV="$4"
|
||||||
PIP_PROXY= PROXY_HOST= GITHUB_PROXY= GITHUB_TOKEN=
|
PIP_PROXY= PROXY_HOST= GITHUB_PROXY= GITHUB_TOKEN=
|
||||||
source {UPDATER!s}
|
source {UPDATER!s}
|
||||||
INFO() {{ :; }}
|
INFO() {{ :; }}
|
||||||
@@ -825,7 +835,7 @@ def test_updater_exposes_explicit_result(
|
|||||||
)
|
)
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["bash", "-c", script, "updater-test", str(tmp_path / "config"), mode, install_result],
|
["bash", "-c", script, "updater-test", str(tmp_path / "config"), mode, install_result, dev_update],
|
||||||
text=True,
|
text=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=True,
|
check=True,
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ def _docker_manager(monkeypatch, tmp_path: Path):
|
|||||||
"FRONTEND_PATH": tmp_path / "public",
|
"FRONTEND_PATH": tmp_path / "public",
|
||||||
"VENV_PATH": tmp_path / "venv",
|
"VENV_PATH": tmp_path / "venv",
|
||||||
"UV_BIN": tmp_path / "uv",
|
"UV_BIN": tmp_path / "uv",
|
||||||
|
"MOVIEPILOT_AUTO_UPDATE": False,
|
||||||
|
"AUTO_UPDATE_RESOURCE": True,
|
||||||
"PIP_PROXY": "",
|
"PIP_PROXY": "",
|
||||||
"PROXY_HOST": "",
|
"PROXY_HOST": "",
|
||||||
}
|
}
|
||||||
@@ -52,6 +54,52 @@ def _response(payload, status_code=200):
|
|||||||
return SimpleNamespace(status_code=status_code, json=lambda: payload)
|
return SimpleNamespace(status_code=status_code, json=lambda: payload)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_reads_live_auto_update_setting_without_discarding_cached_update(monkeypatch, tmp_path):
|
||||||
|
"""切换提醒设置立即反映到状态,缓存版本仍供手动升级使用。"""
|
||||||
|
manager = _manager(monkeypatch, tmp_path)
|
||||||
|
manager._write_state(state="available", version="v3.1.0", can_update=True)
|
||||||
|
for enabled in (True, False, True):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
update_module, "get_runtime_setting",
|
||||||
|
lambda key: tmp_path if key == "TEMP_PATH" else enabled,
|
||||||
|
)
|
||||||
|
status = manager.get_status()
|
||||||
|
assert status.auto_update is enabled
|
||||||
|
assert status.state == "available"
|
||||||
|
assert status.version == "v3.1.0"
|
||||||
|
assert status.can_update is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("auto_update", [False, True])
|
||||||
|
@pytest.mark.parametrize("auto_update_resource", [False, True])
|
||||||
|
def test_scheduled_check_respects_independent_switches(
|
||||||
|
monkeypatch, tmp_path, auto_update, auto_update_resource
|
||||||
|
):
|
||||||
|
"""自动检查只访问已开启的目标;手动检查仍可访问两类更新。"""
|
||||||
|
manager = _manager(monkeypatch, tmp_path)
|
||||||
|
values = {
|
||||||
|
"TEMP_PATH": tmp_path,
|
||||||
|
"MOVIEPILOT_AUTO_UPDATE": auto_update,
|
||||||
|
"AUTO_UPDATE_RESOURCE": auto_update_resource,
|
||||||
|
}
|
||||||
|
monkeypatch.setattr(update_module, "get_runtime_setting", values.get)
|
||||||
|
checked = []
|
||||||
|
monkeypatch.setattr(manager, "_check_application", lambda: checked.append("application"))
|
||||||
|
monkeypatch.setattr(manager, "_check_resources", lambda: checked.append("resources"))
|
||||||
|
|
||||||
|
status = manager.check_scheduled()
|
||||||
|
assert checked == [
|
||||||
|
target for target, enabled in (("application", auto_update), ("resources", auto_update_resource))
|
||||||
|
if enabled
|
||||||
|
]
|
||||||
|
assert status.auto_update is auto_update
|
||||||
|
assert status.auto_update_resource is auto_update_resource
|
||||||
|
|
||||||
|
checked.clear()
|
||||||
|
manager.check()
|
||||||
|
assert checked == ["application", "resources"]
|
||||||
|
|
||||||
|
|
||||||
def test_check_exposes_new_stable_release(monkeypatch, tmp_path):
|
def test_check_exposes_new_stable_release(monkeypatch, tmp_path):
|
||||||
manager = _manager(monkeypatch, tmp_path)
|
manager = _manager(monkeypatch, tmp_path)
|
||||||
logs = []
|
logs = []
|
||||||
|
|||||||
@@ -538,8 +538,9 @@ def test_btrfs_fsid_dedup_setting_is_opt_in():
|
|||||||
assert ConfigModel(BTRFS_FSID_DEDUP="true").BTRFS_FSID_DEDUP is True
|
assert ConfigModel(BTRFS_FSID_DEDUP="true").BTRFS_FSID_DEDUP is True
|
||||||
|
|
||||||
|
|
||||||
def test_auto_update_mode_is_normalized(monkeypatch):
|
@pytest.mark.parametrize("mode", ["release", "dev", " DEV ", "RELEASE"])
|
||||||
"""自动更新仅保留 true、dev 和 false 三种运行模式。"""
|
def test_auto_update_mode_is_normalized(monkeypatch, mode):
|
||||||
|
"""旧模式规范化为布尔 true,已有的新 Dev 偏好不被覆盖。"""
|
||||||
updates = []
|
updates = []
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
Settings,
|
Settings,
|
||||||
@@ -549,14 +550,51 @@ def test_auto_update_mode_is_normalized(monkeypatch):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Settings(MOVIEPILOT_AUTO_UPDATE="release").MOVIEPILOT_AUTO_UPDATE == "false"
|
config = Settings(MOVIEPILOT_AUTO_UPDATE=mode, MOVIEPILOT_UPDATE_DEV=False)
|
||||||
assert Settings(MOVIEPILOT_AUTO_UPDATE="true").MOVIEPILOT_AUTO_UPDATE == "true"
|
assert config.MOVIEPILOT_AUTO_UPDATE is True
|
||||||
assert Settings(MOVIEPILOT_AUTO_UPDATE="dev").MOVIEPILOT_AUTO_UPDATE == "dev"
|
assert config.MOVIEPILOT_UPDATE_DEV is False
|
||||||
assert updates == [
|
assert updates == [
|
||||||
("MOVIEPILOT_AUTO_UPDATE", "release", "false"),
|
("MOVIEPILOT_AUTO_UPDATE", mode, True),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_dev_tracking_is_migrated_once(monkeypatch, tmp_path):
|
||||||
|
"""首次拆分配置时持久化两个开关,重读后继续保留 Dev 跟踪。"""
|
||||||
|
env_file = tmp_path / "app.env"
|
||||||
|
env_file.write_text("MOVIEPILOT_AUTO_UPDATE='dev'\n", encoding="utf-8")
|
||||||
|
monkeypatch.setattr("app.runtime.config.get_env_path", lambda: env_file)
|
||||||
|
config = Settings(_env_file=env_file)
|
||||||
|
assert config.MOVIEPILOT_AUTO_UPDATE is True
|
||||||
|
assert config.MOVIEPILOT_UPDATE_DEV is True
|
||||||
|
assert "MOVIEPILOT_AUTO_UPDATE='true'" in env_file.read_text(encoding="utf-8")
|
||||||
|
assert "MOVIEPILOT_UPDATE_DEV='true'" in env_file.read_text(encoding="utf-8")
|
||||||
|
reloaded = Settings(_env_file=env_file)
|
||||||
|
assert reloaded.MOVIEPILOT_AUTO_UPDATE is True
|
||||||
|
assert reloaded.MOVIEPILOT_UPDATE_DEV is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("enabled", [True, False, "true", "false"])
|
||||||
|
def test_update_switches_remain_independent_booleans(enabled):
|
||||||
|
"""部署设置与调度快照仅暴露布尔值,Dev 跟踪不影响自动检查。"""
|
||||||
|
from app.startup.composition.configuration import build_scheduler_runtime_config
|
||||||
|
|
||||||
|
expected = str(enabled).lower() == "true"
|
||||||
|
config = Settings(MOVIEPILOT_AUTO_UPDATE=enabled, MOVIEPILOT_UPDATE_DEV=not expected)
|
||||||
|
assert config.MOVIEPILOT_AUTO_UPDATE is expected
|
||||||
|
assert config.MOVIEPILOT_UPDATE_DEV is not expected
|
||||||
|
assert build_scheduler_runtime_config(config).auto_update is expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["dev", "release", True, False])
|
||||||
|
def test_update_setting_normalizes_auto_update_on_save(monkeypatch, value):
|
||||||
|
"""设置写入入口与启动读取入口使用同一套布尔转换规则。"""
|
||||||
|
config = Settings(MOVIEPILOT_AUTO_UPDATE=False, MOVIEPILOT_UPDATE_DEV=False)
|
||||||
|
monkeypatch.setattr(Settings, "update_env_config", lambda *_args: (True, ""))
|
||||||
|
config.update_setting("MOVIEPILOT_AUTO_UPDATE", value)
|
||||||
|
assert config.MOVIEPILOT_AUTO_UPDATE is (value is not False)
|
||||||
|
assert config.MOVIEPILOT_UPDATE_DEV is False
|
||||||
|
|
||||||
|
|
||||||
def test_space_usage_default_path_does_not_read_fsid():
|
def test_space_usage_default_path_does_not_read_fsid():
|
||||||
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
||||||
paths = [Path(tmp1), Path(tmp2)]
|
paths = [Path(tmp1), Path(tmp2)]
|
||||||
|
|||||||
Reference in New Issue
Block a user