メインコンテンツへスキップ
1320文字
7分
編集

GitHub releases APIを用いたXR Animatorの自動更新

GitHub Releasesにある最新のリリース情報は固定のURLから取得できる。これを使って自動更新やUI上から更新をサポートしていないXR Animatorの自動更新をサポートする。

#最新のリリース情報の取得

GitHub Releasesでは、次のAPIから最新のリリース情報を取得できる。

text
https://api.github.com/repos/${OWNER}/${REPO}/releases/latest

XR Animatorの場合は、次のURLになる。

https://api.github.com/repos/ButzYung/SystemAnimatorOnline/releases/latest

レスポンスは次のような形式になっており、browser_download_urlからダウンロードURLを取得できる。

json
{
   // ...重要な箇所以外は省略
  "tag_name": "XR-Animator_v0.33.0",
  "published_at": "2025-11-16T16:11:13Z",
  "assets": [
        {
            // ...
            "name": "XR-Animator_v0.33.0_linux-x64.zip",
            "browser_download_url": "https://github.com/...."
        },
        {
            // ... 
            "name": "XR-Animator_v0.33.0_macos-arm64.zip",
        }

#アセットのダウンロード

GitHub Releases APIで取得したデータから目的のアセットのダウンロードURLを取得する。

例えば、XR Animatorの最新のWindows版アセットのダウンロードURLは次のように取得できる。

shell
curl -s https://api.github.com/repos/ButzYung/SystemAnimatorOnline/releases/latest | \
   jq -r '.assets[] | select(.name | contains("windows")) | .browser_download_url '

このコマンドでは、jqコマンドでassets配列の中からnamewindowsを含むアセットのbrowser_download_urlを取得している。目的のアセットに合わせて、jqのクエリを変更する。

取得されたURLに対してダウンロードを行えば、目的のアセットが手に入る。

shell
curl -sSO https://github.com/ButzYung/SystemAnimatorOnline/releases/download/XR-Animator_v0.33.0/XR-Animator_v0.33.0_windows.zip

ダウンロードされるファイル名を変更したい場合は、-Oの代わりに小文字の-oオプションを使い-o <filename>とする。-sSは標準出力を抑制しつつエラーは表示するおまじないなので無くてもいい。

#XR Animatorの自動更新

ここまでのリリース情報の取得方法とXR Animatorの仕様を組み合わせると、次のようなシェルスクリプトで自動更新ができる。

このスクリプトでは実行時に毎回リリースを確認し、更新があれば自動的にインストールを行う。

bash
#!/bin/bash

set -euo pipefail

# Constants
readonly BASE_DIR="${XR_ANIMATOR_DIR:-./XR-Animator}"
readonly API_URL="https://api.github.com/repos/ButzYung/SystemAnimatorOnline/releases/latest"
readonly VERSION_FILE="${BASE_DIR}/.version"
readonly CONFIG_FILE_NAME="XR Animator.js"

# Global variables
PLATFORM=""
RELEASE_JSON=""
LATEST_TAG=""
LATEST_VERSION=""

# Cleanup on error
cleanup() {
    local exit_code=$?
    if [ -n "${TEMP_DIR:-}" ] && [ -d "${TEMP_DIR:-}" ]; then
        rm -rf "${TEMP_DIR}" 2>/dev/null || true
    fi
    if [ -n "${ZIP_FILE:-}" ] && [ -f "${ZIP_FILE:-}" ]; then
        rm -f "${ZIP_FILE}" 2>/dev/null || true
    fi
    exit "${exit_code}"
}
trap cleanup EXIT INT TERM

# Detect platform
detect_platform() {
    case "$(uname -s)" in
        MINGW*|MSYS*|CYGWIN*)
            PLATFORM="windows"
            ;;
        Linux*)
            PLATFORM="linux"
            ;;
        Darwin*)
            PLATFORM="macOS"
            ;;
        *)
            echo "Error: Unsupported OS" >&2
            exit 1
            ;;
    esac
}

# Extract version number from tag name
extract_version_from_tag() {
    local tag="$1"
    echo "${tag}" | sed -n 's/.*v\([0-9.]*\)/\1/p' | sed 's/^/v/'
}

# Fetch latest release information
fetch_latest_release() {
    if [ -z "${RELEASE_JSON:-}" ]; then
        RELEASE_JSON=$(curl -sf "${API_URL}" || {
            echo "Error: Failed to fetch release information from GitHub API" >&2
            exit 1
        })
        LATEST_TAG=$(echo "${RELEASE_JSON}" | jq -r '.tag_name // empty')
        if [ -z "${LATEST_TAG}" ]; then
            echo "Error: Failed to get release tag" >&2
            exit 1
        fi
        LATEST_VERSION=$(extract_version_from_tag "${LATEST_TAG}")
    fi
}

# Get current version
get_current_version() {
    if [ -f "${VERSION_FILE}" ]; then
        cat "${VERSION_FILE}" | tr -d '[:space:]'
    fi
}

# Check if existing installation exists
check_existing_installation() {
    find "${BASE_DIR}" -maxdepth 1 -type d -name "XR Animator - electron-*" 2>/dev/null | head -n 1
}

# Check if update is needed
check_update_needed() {
    local current_version
    current_version=$(get_current_version)
    
    local electron_dir
    electron_dir=$(check_existing_installation)
    
    if [ -z "${current_version}" ]; then
        # .version file does not exist
        fetch_latest_release
        
        if [ -n "${electron_dir}" ]; then
            # Existing installation found, assume it matches latest version
            echo "${LATEST_VERSION}" > "${VERSION_FILE}"
            return 1  # No update needed
        else
            return 0  # Update needed
        fi
    else
        # Current version exists, compare with latest tag
        fetch_latest_release
        
        if [ "${current_version}" != "${LATEST_VERSION}" ]; then
            return 0  # Update needed
        else
            return 1  # No update needed
        fi
    fi
}

# Get download URL
get_download_url() {
    fetch_latest_release
    echo "${RELEASE_JSON}" | jq -r ".assets[] | select(.name | contains(\"${PLATFORM}\")) | .browser_download_url" | head -n 1
}

# Copy config file
copy_config_file() {
    local old_gadget="$1"
    local new_gadget="$2"
    
    if [ -z "${old_gadget}" ] || [ -z "${new_gadget}" ] || [ "${old_gadget}" = "${new_gadget}" ]; then
        return 0
    fi
    
    local old_config="${old_gadget}/TEMP/_config_local/${CONFIG_FILE_NAME}"
    local new_config="${new_gadget}/TEMP/_config_local/${CONFIG_FILE_NAME}"
    
    if [ -f "${old_config}" ] && [ ! -f "${new_config}" ]; then
        mkdir -p "$(dirname "${new_config}")"
        cp "${old_config}" "${new_config}"
    fi
}

# Perform update
perform_update() {
    local download_url="$1"
    
    if [ -z "${download_url}" ]; then
        echo "Error: Failed to get download URL" >&2
        exit 1
    fi
    
    fetch_latest_release
    
    # Temporary directory and ZIP file paths
    ZIP_FILE="${BASE_DIR}/update.zip"
    TEMP_DIR="${BASE_DIR}/.update_temp"
    
    # Download
    echo "Downloading latest version..."
    curl -sfSL -o "${ZIP_FILE}" "${download_url}" || {
        echo "Error: Download failed" >&2
        exit 1
    }
    
    # Extract to temporary directory
    rm -rf "${TEMP_DIR}"
    mkdir -p "${TEMP_DIR}"
    unzip -q "${ZIP_FILE}" -d "${TEMP_DIR}" || {
        echo "Error: Failed to extract ZIP file" >&2
        exit 1
    }
    rm -f "${ZIP_FILE}"
    
    # Backup config file (before update)
    local old_gadget
    old_gadget=$(find "${BASE_DIR}" -maxdepth 1 -type d -name "AT_SystemAnimator_*" 2>/dev/null | sort | head -n 1)
    
    # Remove old directories
    echo "Removing old files..."
    find "${BASE_DIR}" -maxdepth 1 -type d -name "XR Animator - electron-*" -exec rm -rf {} \; 2>/dev/null || true
    find "${BASE_DIR}" -maxdepth 1 -type d -name "AT_SystemAnimator_*" -exec rm -rf {} \; 2>/dev/null || true
    find "${BASE_DIR}" -maxdepth 1 -type d -name "accessories" -exec rm -rf {} \; 2>/dev/null || true
    
    # Move new files
    echo "Installing new files..."
    find "${TEMP_DIR}" -maxdepth 1 -type d -name "XR Animator - electron-*" -exec mv {} "${BASE_DIR}/" \; 2>/dev/null || true
    find "${TEMP_DIR}" -maxdepth 1 -type d -name "AT_SystemAnimator_*" -exec mv {} "${BASE_DIR}/" \; 2>/dev/null || true
    
    if [ -d "${TEMP_DIR}/accessories" ]; then
        [ -d "${BASE_DIR}/accessories" ] && rm -rf "${BASE_DIR}/accessories"
        mv "${TEMP_DIR}/accessories" "${BASE_DIR}/"
    fi
    
    if [ -f "${TEMP_DIR}/readme.txt" ]; then
        mv "${TEMP_DIR}/readme.txt" "${BASE_DIR}/" 2>/dev/null || true
    fi
    
    # Remove temporary directory
    rm -rf "${TEMP_DIR}"
    
    # Copy config file (after update)
    local new_gadget
    new_gadget=$(find "${BASE_DIR}" -maxdepth 1 -type d -name "AT_SystemAnimator_*" 2>/dev/null | sort | tail -n 1)
    if [ -n "${old_gadget}" ] && [ -n "${new_gadget}" ]; then
        copy_config_file "${old_gadget}" "${new_gadget}"
    fi
    
    # Update .version file
    echo "${LATEST_VERSION}" > "${VERSION_FILE}"
    echo "Update completed: ${LATEST_VERSION}"
}

# Find Electron executable
find_electron_executable() {
    local electron_dir="$1"
    local exe_path="${electron_dir}/electron.exe"
    
    if [ ! -f "${exe_path}" ]; then
        exe_path=$(find "${electron_dir}" -maxdepth 1 -type f \( -name "electron" -o -name "electron.exe" \) 2>/dev/null | head -n 1)
    fi
    
    if [ -z "${exe_path}" ] || [ ! -f "${exe_path}" ]; then
        echo "Error: Electron executable not found" >&2
        exit 1
    fi
    
    echo "${exe_path}"
}

# Main function
main() {
    # Check required commands
    for cmd in curl jq unzip find; do
        if ! command -v "${cmd}" >/dev/null 2>&1; then
            echo "Error: Command '${cmd}' not found" >&2
            exit 1
        fi
    done
    
    detect_platform
    
    # Check for updates
    if check_update_needed; then
        local download_url
        download_url=$(get_download_url)
        
        if [ -z "${download_url}" ]; then
            echo "Error: Download URL for ${PLATFORM} not found" >&2
            exit 1
        fi
        
        perform_update "${download_url}"
    else
        echo "No update needed"
    fi
    
    # Launch Electron
    local electron_dir
    electron_dir=$(find "${BASE_DIR}" -maxdepth 1 -type d -name "XR Animator - electron-*" 2>/dev/null | sort | tail -n 1)
    
    if [ -z "${electron_dir}" ]; then
        echo "Error: Electron directory not found" >&2
        exit 1
    fi
    
    local electron_exe
    electron_exe=$(find_electron_executable "${electron_dir}")
    
    exec "${electron_exe}" "$@"
}

main "$@"