自从 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
这个文件夹即可。