#!/bin/bash # shellcheck shell=bash # curl 选项与请求头保存在空格分隔的参数串中,调用时需要展开为多个参数。 # shellcheck disable=SC2086 # 归档按单个 MoviePilot-* 根目录处理;多个匹配项不在该条件表达式的支持范围。 # shellcheck disable=SC2144 Green="\033[32m" Red="\033[31m" Yellow='\033[33m' Font="\033[0m" INFO="[${Green}INFO${Font}]" ERROR="[${Red}ERROR${Font}]" WARN="[${Yellow}WARN${Font}]" function INFO() { echo -e "${INFO} ${1}" } function ERROR() { echo -e "${ERROR} ${1}" } function WARN() { echo -e "${WARN} ${1}" } # 设置虚拟环境路径(兼容群晖等系统必须这样配置) VENV_PATH="${VENV_PATH:-/opt/venv}" export PATH="${VENV_PATH}/bin:$PATH" UV_BIN="${UV_BIN:-/usr/local/bin/uv}" CONFIG_DIR="${CONFIG_DIR:-/config}" APP_DIR=/app PUBLIC_DIR=/public # Dev 更新只需要跨同一容器重启恢复;标记必须和 /app、/public 位于同一容器可写层, # 避免容器重建后 /config 遗留旧标记阻断新镜像启动。 UPDATE_PENDING_FILE="${MOVIEPILOT_UPDATE_PENDING_FILE:-/var/lib/moviepilot/update/__update_pending__}" UPDATE_PREVIOUS_APP="${APP_DIR}.__update_previous__" UPDATE_PREVIOUS_PUBLIC="${PUBLIC_DIR}.__update_previous__" # 镜像构建时写入的系统基线;旧镜像缺少该文件时按机制引入前的下限比较。 SYSTEM_BASELINE_FILE="${MOVIEPILOT_SYSTEM_BASELINE_FILE:-/etc/moviepilot/system-baseline}" LEGACY_SYSTEM_BASELINE="2026-01-01" function apply_package_cache_env() { PACKAGE_CACHE_ROOT="${PACKAGE_CACHE_ROOT:-${CONFIG_DIR}/.cache}" export PACKAGE_CACHE_ROOT export UV_CACHE_DIR="${UV_CACHE_DIR:-${PACKAGE_CACHE_ROOT}/uv}" mkdir -p "${UV_CACHE_DIR}" } apply_package_cache_env PACKAGE_ENV=() UV_OPTIONS=() MOVIEPILOT_UPDATE_RESULT="noop" UPDATE_RECOVERY_REQUIRED="false" UPDATE_RECOVERY_COMPLETED="false" UPDATE_RECOVERY_BLOCKED="false" DEPENDENCY_SYNC_ATTEMPTED="false" PACKAGE_ROUTE_READY="false" function set_package_proxy_env() { PACKAGE_ENV=() if [[ -n "${PROXY_HOST}" ]]; then PACKAGE_ENV=( "HTTP_PROXY=${PROXY_HOST}" "HTTPS_PROXY=${PROXY_HOST}" "http_proxy=${PROXY_HOST}" "https_proxy=${PROXY_HOST}" ) fi } # 按调用时配置拼接 GitHub 下载地址;兼容无尾斜杠、路径前缀和首尾空白。 function github_download_url() { local proxy proxy="$(printf '%s' "${GITHUB_PROXY:-}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" if [[ -z "${proxy}" ]]; then printf '%s\n' "$1" return fi if [[ "${proxy}" != http://* && "${proxy}" != https://* ]]; then proxy="http://${proxy}" fi printf '%s\n' "${proxy%/}/$1" } # 下载归档到临时目录;失败最多尝试三次,成功后归一化后端根目录名称。 function download_and_unzip() { local retries=0 local max_retries=3 local url="$1" local target_dir="$2" INFO "→ 正在下载 ${url}..." while [ $retries -lt $max_retries ]; do if curl ${CURL_OPTIONS} "${url}" ${CURL_HEADERS} | busybox unzip -d ${TMP_PATH} - > /dev/null; then if [ -e ${TMP_PATH}/MoviePilot-* ]; then mv ${TMP_PATH}/MoviePilot-* ${TMP_PATH}/"${target_dir}" fi break else WARN "下载 ${url} 失败,正在进行第 $((retries + 1)) 次重试..." retries=$((retries + 1)) fi done if [ $retries -eq $max_retries ]; then ERROR "下载 ${url} 失败,已达到最大重试次数!" return 1 else return 0 fi } # 输出去除空白后的 YYYY-MM-DD 基线;格式无效时返回失败。 function normalize_system_baseline() { local value value="$(printf '%s' "$1" | tr -d '[:space:]')" [[ "${value}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || return 1 printf '%s\n' "${value}" } # 目标版本要求的系统基线高于当前镜像时拒绝更新:系统组件只能随镜像更新。 # 未声明 MIN_SYSTEM_BASELINE 的旧版本视为无要求;声明无效时同样拒绝,保留当前版本。 function check_system_baseline() { local version_file="$1" local required_raw local required local current grep -Eq '^MIN_SYSTEM_BASELINE[[:space:]]*=' "${version_file}" 2>/dev/null || return 0 required_raw="$(sed -n "s/^MIN_SYSTEM_BASELINE[[:space:]]*=[[:space:]]*['\"]\([^'\"]*\)['\"].*/\1/p" "${version_file}" | head -n 1)" if ! required="$(normalize_system_baseline "${required_raw}")"; then WARN "更新包声明的系统基线无效(${required_raw}),已跳过本次更新并保留当前版本" return 1 fi current="$(normalize_system_baseline "$(cat "${SYSTEM_BASELINE_FILE}" 2>/dev/null)")" || current="" # 基线均为 YYYY-MM-DD,字符串顺序与日期顺序一致。 if [[ "${required}" > "${current:-${LEGACY_SYSTEM_BASELINE}}" ]]; then WARN "该版本包含系统组件安全更新,需要先更新 Docker 镜像(要求系统基线 ${required},当前 ${current:-未标记}),已跳过本次更新并保留当前版本" return 1 fi return 0 } function sync_project_dependencies() { INFO "检测到依赖变化,正在更新虚拟环境..." configure_package_route || return 1 PACKAGE_ROUTE_READY="true" DEPENDENCY_SYNC_ATTEMPTED="true" INFO "依赖源:${PACKAGE_LOG}" if ! sync_project_dependencies_for "${TMP_PATH}/App"; then ERROR "依赖同步失败,当前程序依赖未完成更新" return 1 fi INFO "依赖更新成功" } function dependency_manifests_changed() { ! cmp -s "${APP_DIR}/pyproject.toml" "${TMP_PATH}/App/pyproject.toml" \ || ! cmp -s "${APP_DIR}/uv.lock" "${TMP_PATH}/App/uv.lock" } function set_update_pending() { local state="${1:-prepared}" local pending_dir local pending_tmp pending_dir="$(dirname "${UPDATE_PENDING_FILE}")" pending_tmp="${pending_dir}/.__update_pending__.tmp.$$" mkdir -p "${pending_dir}" \ && printf '%s\n' "${state}" > "${pending_tmp}" \ && mv -f "${pending_tmp}" "${UPDATE_PENDING_FILE}" || { rm -f "${pending_tmp}" return 1 } } function clear_update_pending() { rm -f "${UPDATE_PENDING_FILE}" } function update_pending_state() { [ -f "${UPDATE_PENDING_FILE}" ] || return 1 tr -d '\r\n' < "${UPDATE_PENDING_FILE}" } function sync_project_dependencies_for() { local project_dir="$1" local legacy_runtime_selector local runtime_group local runtime_selector local -a uv_cmd=( "${UV_BIN}" sync --project "${project_dir}" --locked --inexact --no-dev --no-install-project --python "${VENV_PATH}/bin/python3" ) runtime_group="" if grep -Eq '^runtime-(standard|free-threaded)[[:space:]]*=' "${project_dir}/pyproject.toml"; then runtime_selector="${project_dir}/app/runtime/dependencies/profile.py" legacy_runtime_selector="${project_dir}/app/runtime/dependencies.py" if [ -f "${runtime_selector}" ]; then runtime_group="$( PYTHONPATH="${project_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ "${VENV_PATH}/bin/python3" -m app.runtime.dependencies.profile )" || return 1 elif [ -f "${legacy_runtime_selector}" ]; then runtime_group="$( PYTHONPATH="${project_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ "${VENV_PATH}/bin/python3" "${legacy_runtime_selector}" )" || return 1 else return 1 fi [ -n "${runtime_group}" ] || return 1 fi if [ -n "${runtime_group}" ]; then uv_cmd+=(--no-default-groups --group "${runtime_group}") fi uv_cmd+=("${UV_OPTIONS[@]}") env "${PACKAGE_ENV[@]}" \ "UV_PROJECT_ENVIRONMENT=${VENV_PATH}" \ "UV_LINK_MODE=copy" "${uv_cmd[@]}" } function restore_project_dependencies() { if [ "${PACKAGE_ROUTE_READY}" != "true" ]; then configure_package_route || return 1 PACKAGE_ROUTE_READY="true" fi INFO "→ 正在恢复更新前的程序依赖..." if ! sync_project_dependencies_for "${APP_DIR}"; then ERROR "依赖回滚失败,保留更新事务标记以便下次启动继续恢复" return 1 fi INFO "→ 更新前的程序依赖已恢复" } function cleanup_previous_payload() { rm -rf "${UPDATE_PREVIOUS_APP}" "${UPDATE_PREVIOUS_PUBLIC}" } function finalize_update_transaction() { cleanup_previous_payload || return 1 clear_update_pending } function restore_previous_payload() { local failed="false" if [ -e "${UPDATE_PREVIOUS_APP}" ]; then if [ -e "${APP_DIR}" ] && ! rm -rf "${APP_DIR}"; then failed="true" elif ! mv "${UPDATE_PREVIOUS_APP}" "${APP_DIR}"; then failed="true" fi fi if [ -e "${UPDATE_PREVIOUS_PUBLIC}" ]; then if [ -e "${PUBLIC_DIR}" ] && ! rm -rf "${PUBLIC_DIR}"; then failed="true" elif ! mv "${UPDATE_PREVIOUS_PUBLIC}" "${PUBLIC_DIR}"; then failed="true" fi fi [ "${failed}" = "false" ] } function rollback_update_transaction() { local failed="false" if ! restore_previous_payload; then failed="true" fi if [ "${DEPENDENCY_SYNC_ATTEMPTED}" = "true" ] && ! restore_project_dependencies; then failed="true" fi if [ "${failed}" = "true" ]; then UPDATE_RECOVERY_REQUIRED="true" return 1 fi if ! finalize_update_transaction; then UPDATE_RECOVERY_REQUIRED="true" return 1 fi return 0 } function recover_pending_update() { local state state="$(update_pending_state 2>/dev/null || true)" [ -n "${state}" ] || return 0 if [ "${state}" = "committed" ]; then INFO "→ 清理已完成的容器更新事务" if ! finalize_update_transaction; then WARN "→ 已完成更新的旧代际备份清理失败,保留事务标记以便下次启动重试" return 1 fi return 0 fi if [ "${state}" = "dependencies" ] || [ "${state}" = "blocked" ]; then if [ ! -e "${APP_DIR}" ] || [ ! -e "${PUBLIC_DIR}" ]; then # 目录切换中断可能留下不完整的当前载荷;这种情况仍需恢复完整旧代际。 if [ ! -e "${UPDATE_PREVIOUS_APP}" ] || [ ! -e "${UPDATE_PREVIOUS_PUBLIC}" ]; then ERROR "→ 当前与更新前载荷均不完整,无法安全恢复" UPDATE_RECOVERY_REQUIRED="true" return 1 fi WARN "→ 当前更新载荷不完整,恢复更新前版本" else # 依赖阶段可能已经切换到包含新迁移的 /app;此时不能恢复旧代际, # 否则数据库已经前进时会把旧程序交给未知的 Alembic revision。 WARN "→ 更新事务停留在依赖阶段,保留当前程序并等待启动前依赖自愈,不回退旧版本" if [ "${state}" = "dependencies" ] && ! set_update_pending blocked; then ERROR "→ 无法记录保留当前程序的更新恢复状态" return 1 fi UPDATE_RECOVERY_BLOCKED="true" UPDATE_RECOVERY_COMPLETED="true" return 0 fi fi WARN "→ 检测到未完成的容器更新事务,正在恢复旧版本" rollback_update_transaction || return 1 UPDATE_RECOVERY_COMPLETED="true" INFO "→ 未完成的容器更新事务已恢复" } function existing_resource_dir() { local resource_source_dir="${APP_DIR}/app/application/site" for legacy_resource_dir in "${APP_DIR}/app/infrastructure" "${APP_DIR}/app/adapters/network" "${APP_DIR}/app/helper"; do if [ ! -d "${resource_source_dir}" ] && [ -d "${legacy_resource_dir}" ]; then resource_source_dir="${legacy_resource_dir}" fi done printf '%s\n' "${resource_source_dir}" } # 资源只写暂存目录;连接抖动有限重试,耗尽后才回退到当前已安装资源。 function download_staged_resource() { local url="$1" local destination="$2" local fallback="$3" local label="$4" if curl ${CURL_OPTIONS} --fail --connect-timeout 10 --max-time 180 \ --retry 2 --retry-delay 1 --retry-all-errors "${url}" -o "${destination}" \ && [ -s "${destination}" ]; then return 0 fi rm -f "${destination}" if [ -f "${fallback}" ]; then cp -a "${fallback}" "${destination}" return $? fi ERROR "${label} 下载失败且没有可用旧资源" return 1 } # 暂存插件与站点资源,规范化镜像前缀后下载匹配当前 Python ABI 的资源。 function stage_runtime_payload() { local stage_app="${TMP_PATH}/App" local stage_plugin_dir="${stage_app}/app/plugins" local stage_resource_dir="${stage_app}/app/application/site" local resource_source_dir local resource_file local python_version local arch local arch_suffix local sites_file [ -f "${stage_app}/version.py" ] || return 1 [ -f "${stage_app}/pyproject.toml" ] || return 1 [ -f "${stage_app}/uv.lock" ] || return 1 [ -f "${TMP_PATH}/dist/index.html" ] || return 1 if [ -e "${stage_plugin_dir}" ] && [ ! -d "${stage_plugin_dir}" ]; then ERROR "插件运行目录不是目录" return 1 fi mkdir -p "${stage_plugin_dir}" || return 1 # 新版后端归档提供宿主兼容入口;仅迁移旧目录中的插件内容,避免旧版入口覆盖它。 if ! find "${stage_plugin_dir}" -mindepth 1 -maxdepth 1 \ ! -name "__init__.py" -exec rm -rf -- {} \;; then return 1 fi if [ -d "${APP_DIR}/app/plugins" ]; then if ! find "${APP_DIR}/app/plugins" -mindepth 1 -maxdepth 1 \ ! -name "__init__.py" -exec cp -a {} "${stage_plugin_dir}/" \;; then return 1 fi fi if [ ! -f "${stage_plugin_dir}/__init__.py" ]; then ERROR "插件运行目录缺少 app.plugins 兼容入口" return 1 fi resource_source_dir="$(existing_resource_dir)" mkdir -p "${stage_resource_dir}" || return 1 if [ -f "${resource_source_dir}/user.sites.v3.bin" ] \ && ! cp -a "${resource_source_dir}/user.sites.v3.bin" "${stage_resource_dir}/"; then return 1 fi for resource_file in "${resource_source_dir}"/sites.cp*; do [ -f "${resource_file}" ] || continue cp -a "${resource_file}" "${stage_resource_dir}/" || return 1 done python_version="$("${VENV_PATH}/bin/python3" -c 'import sys, sysconfig; print(f"cpython-{sys.version_info.major}{sys.version_info.minor}{"t" if sysconfig.get_config_var("Py_GIL_DISABLED") == 1 else ""}")')" || return 1 arch="$(uname -m)" if [ "${arch}" = "aarch64" ]; then arch_suffix="aarch64-linux-gnu" else arch_suffix="x86_64-linux-gnu" fi sites_file="sites.${python_version}-${arch_suffix}.so" download_staged_resource \ "$(github_download_url "https://raw.githubusercontent.com/jxxghp/MoviePilot-Resources/main/resources.v3/user.sites.v3.bin")" \ "${stage_resource_dir}/user.sites.v3.bin" \ "${resource_source_dir}/user.sites.v3.bin" \ "user.sites.v3.bin" || return 1 download_staged_resource \ "$(github_download_url "https://raw.githubusercontent.com/jxxghp/MoviePilot-Resources/main/resources.v3/${sites_file}")" \ "${stage_resource_dir}/${sites_file}" \ "${resource_source_dir}/${sites_file}" \ "${sites_file}" || return 1 } function swap_staged_payload() { cleanup_previous_payload || return 1 mv "${APP_DIR}" "${UPDATE_PREVIOUS_APP}" || return 1 if ! mv "${PUBLIC_DIR}" "${UPDATE_PREVIOUS_PUBLIC}"; then mv "${UPDATE_PREVIOUS_APP}" "${APP_DIR}" || true return 1 fi if ! mv "${TMP_PATH}/App" "${APP_DIR}" || ! mv "${TMP_PATH}/dist" "${PUBLIC_DIR}"; then restore_previous_payload || true return 1 fi } # 通过规范化镜像地址准备前后端归档;$1 为后端版本路径,全部准备成功后才切换。 function install_backend_and_download_resources() { if ! download_and_unzip "$(github_download_url "https://github.com/jxxghp/MoviePilot/archive/refs/${1}")" "App"; then WARN "后端程序下载失败,继续使用旧的程序来启动..." return 1 fi INFO "后端程序包准备成功" if ! check_system_baseline "${TMP_PATH}/App/version.py"; then return 1 fi # 检查依赖清单,实际同步延后到所有运行载荷准备完成之后。 INFO "→ 检查依赖变化..." local dependencies_changed="false" if [ -f "${TMP_PATH}/App/pyproject.toml" ] && [ -f "${TMP_PATH}/App/uv.lock" ]; then if dependency_manifests_changed; then dependencies_changed="true" else INFO "依赖无变化,跳过依赖更新" fi else ERROR "更新包缺少 pyproject.toml 或 uv.lock,拒绝替换当前程序" return 1 fi # 如果是"heads/v3.zip",则查找v3开头的最新版本号 if [[ "${1}" == "heads/v3.zip" ]]; then INFO "→ 正在获取前端最新版本号..." releases=$(curl ${CURL_OPTIONS} "https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases" ${CURL_HEADERS} | jq -r '.[].tag_name' | grep "^v3\.") if [ -z "$releases" ]; then WARN "未找到任何v3前端版本,继续启动..." return 1 else frontend_version=$(echo "$releases" | sort -V | tail -n 1) fi INFO "前端最新版本号:${frontend_version}" else INFO "→ 正在获取前端版本号..." # 从后端文件中读取前端版本号 frontend_version=$(sed -n "s/^FRONTEND_VERSION\s*=\s*'\([^']*\)'/\1/p" ${TMP_PATH}/App/version.py) if [[ "${frontend_version}" != *v* ]]; then WARN "前端版本号获取失败,继续启动..." return 1 fi INFO "前端版本号:${frontend_version}" fi if ! download_and_unzip "$(github_download_url "https://github.com/jxxghp/MoviePilot-Frontend/releases/download/${frontend_version}/dist.zip")" "dist"; then WARN "前端程序下载失败,继续使用旧的程序来启动..." return 1 fi INFO "前端程序包准备成功" INFO "→ 正在准备插件和站点资源..." if ! stage_runtime_payload; then ERROR "更新载荷准备失败,当前程序未替换" return 1 fi # 标记必须先于依赖同步和目录切换写入,进程在任一阶段中断后才能恢复旧代际。 if ! set_update_pending prepared; then ERROR "无法记录更新事务,当前程序未替换" return 1 fi if [ "${dependencies_changed}" = "true" ]; then if ! set_update_pending dependencies; then ERROR "无法记录依赖更新事务,当前程序未替换" return 1 fi if ! sync_project_dependencies; then ERROR "依赖同步失败,正在恢复更新前的运行环境" rollback_update_transaction || true return 1 fi fi if ! swap_staged_payload; then ERROR "程序文件切换失败,正在恢复更新前的运行环境" rollback_update_transaction || true return 1 fi if ! set_update_pending committed; then ERROR "无法确认更新事务,正在恢复更新前的运行环境" rollback_update_transaction || true return 1 fi if ! finalize_update_transaction; then WARN "更新完成,但旧程序备份清理失败,保留事务标记以便下次启动重试" fi rm -rf "${TMP_PATH}" MOVIEPILOT_UPDATE_RESULT="updated" INFO "程序更新成功,前端版本:${frontend_version},后端版本:${1}" return 0 } function probe_package_index() { local probe_env=( "UV_NO_CACHE=1" "UV_HTTP_TIMEOUT=5" "UV_HTTP_RETRIES=0" ) local package_index="${1:-}" local use_proxy="${2:-false}" local probe_dir local -a probe_args=(pip install) if [[ "${use_proxy}" = "true" ]]; then probe_env+=( "HTTP_PROXY=${PROXY_HOST}" "HTTPS_PROXY=${PROXY_HOST}" "http_proxy=${PROXY_HOST}" "https_proxy=${PROXY_HOST}" ) fi probe_dir=$(mktemp -d) || return 1 # 包源探针必须使用独立目标目录,避免修改主程序与插件共享的虚拟环境。 probe_args+=(--target "${probe_dir}" --no-deps) if [[ -n "${package_index}" ]]; then probe_args+=(--default-index "${package_index}") fi probe_args+=(pip-hello-world) ( trap 'rm -rf "${probe_dir}"' EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM timeout --kill-after=2s 10s env "${probe_env[@]}" \ "${UV_BIN}" "${probe_args[@]}" > /dev/null 2>&1 ) } function test_connectivity_package() { case "$1" in 0) if [[ -n "${PIP_PROXY}" ]]; then if [[ -n "${PROXY_HOST}" ]]; then probe_package_index "${PIP_PROXY}" true else probe_package_index "${PIP_PROXY}" false fi if [[ $? -eq 0 ]]; then UV_OPTIONS=(--default-index "${PIP_PROXY}") PACKAGE_LOG="镜像代理模式" set_package_proxy_env return 0 fi fi return 1 ;; 1) if [[ -n "${PROXY_HOST}" ]]; then if probe_package_index "" true; then UV_OPTIONS=() PACKAGE_LOG="全局代理模式" set_package_proxy_env return 0 fi fi return 1 ;; 2) PACKAGE_ENV=() UV_OPTIONS=() PACKAGE_LOG="不使用代理" return 0 ;; esac } # 按传入模式检测镜像或全局代理,镜像探测与实际下载共用地址规则。 # 直连模式仅配置参数,不额外探测。 function test_connectivity_github() { case "$1" in 0) if [[ -n "${GITHUB_PROXY}" ]]; then if curl -sL --connect-timeout 5 --max-time 10 "$(github_download_url "https://raw.githubusercontent.com/jxxghp/MoviePilot/main/README.md")" > /dev/null 2>&1; then GITHUB_LOG="镜像代理模式" return 0 fi fi return 1 ;; 1) if [[ -n "${PROXY_HOST}" ]]; then if curl -sL --connect-timeout 5 --max-time 10 -x ${PROXY_HOST} https://raw.githubusercontent.com/jxxghp/MoviePilot/main/README.md > /dev/null 2>&1; then CURL_OPTIONS="-sL -x ${PROXY_HOST}" GITHUB_LOG="全局代理模式" return 0 fi fi return 1 ;; 2) CURL_OPTIONS="-sL" GITHUB_LOG="不使用代理" return 0 ;; esac } function configure_package_route() { local retries=0 while true; do if test_connectivity_package "${retries}"; then return 0 fi retries=$((retries + 1)) done } function run_moviepilot_update() { # 新 Dev 开关独立于自动检查;仅在未配置新开关时兼容首次启动的旧 dev 值。 MOVIEPILOT_UPDATE_RESULT="noop" 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) if [ ! -d "${TMP_PATH}" ]; then TMP_PATH=/tmp/mp_update_path rm -rf "${TMP_PATH}" mkdir -p "${TMP_PATH}" fi retries=0 while true; do if test_connectivity_github "${retries}"; then break fi retries=$((retries + 1)) done INFO "Github:${GITHUB_LOG}" if [ -n "${GITHUB_TOKEN}" ]; then CURL_HEADERS="--oauth2-bearer ${GITHUB_TOKEN}" else CURL_HEADERS="" fi INFO "Dev 更新模式" if ! install_backend_and_download_resources "heads/v3.zip"; then MOVIEPILOT_UPDATE_RESULT="failed" fi rm -rf "${TMP_PATH}" else INFO "没有待安装 Dev 更新,按当前版本启动" fi }