コンテンツにスキップ

VRChat アバター操作

アバター一覧の全件取得

search_avatars は 1 回のリクエストで最大 100 件しか取得できない。全件取得するにはオフセットを進めながらページングする。

from vrchatapi.api.avatars_api import AvatarsApi

avatar_api = AvatarsApi(api_client)

avatars = []
offset = 0
while True:
    results = avatar_api.search_avatars(
        user="me", sort="name", n=100,
        release_status="all", offset=offset
    )
    if not results:
        break
    avatars.extend(results)
    offset += len(results)

サムネイル画像の更新

画像をアップロードし、取得した URL を update_avatar でアバターに紐づける。

from vrchatapi.api.files_api import FilesApi

files_api = FilesApi(api_client)
file = files_api.upload_image(file_path, "avatarimage")

# 最新バージョンの URL を取得
latest_version = max(file.versions, key=lambda v: v.version)
url = latest_version.file.url

avatar_api.update_avatar(
    avatar_id,
    update_avatar_request={"imageUrl": url},
)

レート制限への対応

VRChat API は頻繁なリクエストに対して HTTP 429 を返す。 Retry-After ヘッダーに指定された秒数を待ってからリトライする。

from vrchatapi.rest import ApiException
import time

while True:
    try:
        result = api_call()
        break
    except ApiException as e:
        if e.status == 429:
            retry_after = e.headers.get("Retry-After")
            wait = int(retry_after) + 5 if retry_after else 60
            time.sleep(wait)
        else:
            raise

アップロードとサムネイル更新の両方で 429 が発生しうるため、それぞれにリトライ処理を実装する。

差分チェックで不要アップロードを削減

既存のサムネイルをダウンロードして新画像とピクセル単位で比較し、差分がない場合はアップロードをスキップする。

import os
import urllib.request
from PIL import Image

def download_file(url: str, dst_path: str) -> None:
    req = urllib.request.Request(url, headers={"User-Agent": "MyApp (author, v1.0.0)"})
    with urllib.request.urlopen(req) as web_file, open(dst_path, "wb") as local_file:
        local_file.write(web_file.read())

def is_image_different(path1: str, path2: str) -> bool:
    img1 = Image.open(path1).convert("RGBA")
    img2 = Image.open(path2).convert("RGBA")
    if img1.size != img2.size:
        img2 = img2.resize(img1.size, Image.LANCZOS)
    return any(p1 != p2 for p1, p2 in zip(img1.getdata(), img2.getdata()))

download_file(avatar.image_url, existing_path)
if not is_image_different(new_image_path, existing_path):
    os.remove(existing_path)
    continue  # 変更なし、スキップ
os.remove(existing_path)

サムネイルへのオーバーレイ合成

Pillow を使って画像の特定位置にテキストや画像を合成する。

from PIL import Image, ImageDraw, ImageFont

def insert_text(
    file_path: str,
    text: str,
    font: ImageFont.FreeTypeFont,
    color: tuple = (255, 255, 255),
    position: str = "top_right",
    x_padding: int = 10,
    y_padding: int = 10,
) -> None:
    image = Image.open(file_path)
    draw = ImageDraw.Draw(image)
    text_width = draw.textlength(text, font=font)
    text_height = font.getbbox(text)[3] - font.getbbox(text)[1]

    if position == "top_right":
        x = image.width - text_width - x_padding
        y = y_padding
    elif position == "bottom_right":
        x = image.width - text_width - x_padding
        y = image.height - text_height - (y_padding * 2)
    # 他の位置は同様に実装

    draw.text((x, y), text, font=font, fill=color)
    image.save(file_path)