自從 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("Done")
if __name__ == "__main__":
main()
使用方法#
首先確保你安裝了以下兩個 pip 依賴,在終端執行:
/usr/bin/pip3 install httpx tqdm
新建一個 screensaver.py 文件,將代碼內容拷貝到 screensaver.py 中,並在終端中執行 sudo /usr/python3 screensaver.py
默認單線程下載,你也可以使用 batch 參數進行批量下載:
sudo /usr/python3 screensaver.py --batch 5
所有壁紙下載完成後有 66G,還是挺佔空間的。
P.S. 下載完成後可能需要重新登錄用戶(或重啟)後才能在設置中正常顯示。
P.P.S. 如果需要刪除螢保,清除 /Library/Application\ Support/com.apple.idleassetsd/Customer/4KSDR240FPS
這個文件夾即可。