macOS Sonoma バージョンから、Apple は Mac に新しいスクリーンセーバーを提供しました。
これらのスクリーンセーバーはすべて 4K ドローンビデオで、同時に壁紙として設定できます。壁紙として設定した後、スクリーンセーバーからデスクトップに戻ると、動的な遷移アニメーションがあります。効果は非常に良く、種類も豊富です。
4K 画質のため、非常にスペースを占有します。これらのスクリーンセーバーはシステムにプリロードされていません。プリセットのいくつかを除いて、他は設定でクリックしてプログラムが自動的にダウンロードするのを待つ必要がありますが、ダウンロード速度は非常に遅く、しばしば十数 KB しかありません。
調べたところ、これらのスクリーンセーバーは /Library/Application\ Support/com.apple.idleassetsd/Customer
ディレクトリに保存されており、メタデータは同じディレクトリ内の entries.json
ファイルに保存されています。
それなら簡単です。私は Python スクリプトを書き、これらのスクリーンセーバーを一括でダウンロードできるようにしました。
スクリプトコード#
import json
import asyncio
import sys
from argparse import ArgumentParser
from pathlib import Path
import plistlib
import httpx
from tqdm import tqdm
BASE_PATH = Path("/Library/Application Support/com.apple.idleassetsd/Customer")
DEST_PATH = BASE_PATH / "4KSDR240FPS"
ENTRIES_FILE = BASE_PATH / "entries.json"
LOCALIZABLE_FILE = BASE_PATH / "TVIdleScreenStrings.bundle/zh_CN.lproj/Localizable.nocache.strings"
def load_localizable_strings() -> dict:
strings = {}
if LOCALIZABLE_FILE.exists():
with LOCALIZABLE_FILE.open("rb") as f:
plist_data = plistlib.load(f)
strings.update(plist_data)
return strings
def get_localized_name(key: str, strings: dict) -> str:
return strings.get(key, key)
def download_asset_sync(item: dict, dst: Path):
name = f"{item['categoryName']}: {item['assetName']}"
tqdm.write(f"Downloading: {name}")
try:
with dst.open("wb") as download_file:
with httpx.stream("GET", item["url-4K-SDR-240FPS"], verify=False) as response:
total = int(response.headers.get("Content-Length", 0))
with tqdm(total=total, unit="B", unit_scale=True, unit_divisor=1024, desc=name, position=1, leave=False) as progress:
num_bytes_downloaded = response.num_bytes_downloaded
for chunk in response.iter_bytes():
download_file.write(chunk)
progress.update(response.num_bytes_downloaded - num_bytes_downloaded)
num_bytes_downloaded = response.num_bytes_downloaded
except (httpx.RequestError, httpx.HTTPStatusError) as e:
tqdm.write(f"Error downloading {name}: {e}")
async def download_asset_async(client, item: dict, dst: Path, position: int):
name = f"{item['categoryName']}: {item['assetName']}"
tqdm.write(f"Downloading: {name}")
try:
async with client.stream("GET", item["url-4K-SDR-240FPS"]) as response:
total = int(response.headers.get("Content-Length", 0))
with tqdm(total=total, unit="B", unit_scale=True, unit_divisor=1024, desc=name, position=position, leave=False) as progress:
with dst.open("wb") as download_file:
async for chunk in response.aiter_bytes():
download_file.write(chunk)
progress.update(len(chunk))
except (httpx.RequestError, httpx.HTTPStatusError) as e:
tqdm.write(f"Error downloading {name}: {e}")
async def download_asset_concurrent(items: list, max_concurrent: int = 5):
async with httpx.AsyncClient(verify=False, timeout=30.0) as client:
tasks = []
pending_items = [item for item in items if not (DEST_PATH / f"{item['id']}.mov").exists() and item.get("url-4K-SDR-240FPS")]
# 進捗バーの行を確保
for i in range(min(max_concurrent, len(pending_items))):
print(f"\033[K", end="") # 行をクリア
print() # 行を確保
print(f"\033[{min(max_concurrent, len(pending_items))}A", end="", flush=True) # カーソルを最初の行に移動
for index, item in enumerate(pending_items):
position = (index % max_concurrent) + 1 # 行番号を割り当て(1 から max_concurrent)
tasks.append(download_asset_async(client, item, DEST_PATH / f"{item['id']}.mov", position))
if len(tasks) >= max_concurrent:
await asyncio.gather(*tasks, return_exceptions=True)
tasks = []
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
# 進捗バー領域をクリア
print(f"\033[{min(max_concurrent, len(pending_items))}A", end="", flush=True)
for _ in range(min(max_concurrent, len(pending_items))):
print(f"\033[K", end="") # 行をクリア
print()
def main():
parser = ArgumentParser(description="Download macOS Aerial screensaver assets")
parser.add_argument("--batch", nargs="?", const=5, type=int, metavar="SIZE", help="Use concurrent downloads, 5 tasks by default")
args = parser.parse_args()
if not ENTRIES_FILE.exists():
print(f"Error: {ENTRIES_FILE} not found")
sys.exit(1)
with ENTRIES_FILE.open() as f:
data = json.load(f)
localizable_strings = load_localizable_strings()
categories = {}
for category in data.get("categories", []):
category_name = get_localized_name(category["localizedNameKey"], localizable_strings)
categories[category["id"]] = category_name
for asset in data.get("assets", []):
category_id = asset.get("categories", [""])[0]
asset["categoryName"] = categories.get(category_id, "")
asset["assetName"] = get_localized_name(asset["localizedNameKey"], localizable_strings)
DEST_PATH.mkdir(parents=True, exist_ok=True)
if args.batch:
asyncio.run(download_asset_concurrent(data.get("assets", []), max_concurrent=args.batch))
else:
for item in tqdm(data.get("assets", []), desc="Processing assets", position=0):
dst = DEST_PATH / f"{item['id']}.mov"
if not dst.exists() and item.get("url-4K-SDR-240FPS"):
download_asset_sync(item, dst)
print("完了")
if __name__ == "__main__":
main()
使用方法#
まず、以下の二つの pip 依存関係をインストールしていることを確認し、ターミナルで実行します:
/usr/bin/pip3 install httpx tqdm
新しい screensaver.py ファイルを作成し、コード内容を screensaver.py にコピーして、ターミナルで sudo /usr/python3 screensaver.py
を実行します。
デフォルトではシングルスレッドでダウンロードされますが、バッチパラメータを使用して一括ダウンロードすることもできます:
sudo /usr/python3 screensaver.py --batch 5
すべての壁紙のダウンロードが完了すると、66G になり、かなりのスペースを占有します。
P.S. ダウンロードが完了した後、設定で正常に表示されるようにするには、ユーザーに再ログインする(または再起動する)必要があるかもしれません。
P.P.S. スクリーンセーバーを削除する必要がある場合は、/Library/Application\ Support/com.apple.idleassetsd/Customer/4KSDR240FPS
フォルダを削除してください。