GitHub releases APIを用いたXR Animatorの自動更新
GitHub Releasesにある最新のリリース情報は固定のURLから取得できる。これを使って自動更新やUI上から更新をサポートしていないXR Animatorの自動更新をサポートする。
最新のリリース情報の取得
GitHub Releasesでは、次のAPIから最新のリリース情報を取得できる。
https://api.github.com/repos/${OWNER}/${REPO}/releases/latestXR Animatorの場合は、次のURLになる。
https://api.github.com/repos/ButzYung/SystemAnimatorOnline/releases/latest
レスポンスは次のような形式になっており、browser_download_urlからダウンロードURLを取得できる。
{ // ...重要な箇所以外は省略 "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は次のように取得できる。
curl -s https://api.github.com/repos/ButzYung/SystemAnimatorOnline/releases/latest | \ jq -r '.assets[] | select(.name | contains("windows")) | .browser_download_url 'このコマンドでは、jqコマンドでassets配列の中からnameにwindowsを含むアセットのbrowser_download_urlを取得している。目的のアセットに合わせて、jqのクエリを変更する。
取得されたURLに対してダウンロードを行えば、目的のアセットが手に入る。
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の仕様を組み合わせると、次のようなシェルスクリプトで自動更新ができる。
このスクリプトでは実行時に毎回リリースを確認し、更新があれば自動的にインストールを行う。
#!/bin/bash
set -euo pipefail
# Constantsreadonly 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 variablesPLATFORM=""RELEASE_JSON=""LATEST_TAG=""LATEST_VERSION=""
# Cleanup on errorcleanup() { 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 platformdetect_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 nameextract_version_from_tag() { local tag="$1" echo "${tag}" | sed -n 's/.*v\([0-9.]*\)/\1/p' | sed 's/^/v/'}
# Fetch latest release informationfetch_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 versionget_current_version() { if [ -f "${VERSION_FILE}" ]; then cat "${VERSION_FILE}" | tr -d '[:space:]' fi}
# Check if existing installation existscheck_existing_installation() { find "${BASE_DIR}" -maxdepth 1 -type d -name "XR Animator - electron-*" 2>/dev/null | head -n 1}
# Check if update is neededcheck_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 URLget_download_url() { fetch_latest_release echo "${RELEASE_JSON}" | jq -r ".assets[] | select(.name | contains(\"${PLATFORM}\")) | .browser_download_url" | head -n 1}
# Copy config filecopy_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 updateperform_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 executablefind_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 functionmain() { # 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 "$@"