3.9 KB · 纯文本展示
#!/usr/bin/env python3
"""把 Chrome 书签合并进 Safari 的 Bookmarks.plist(Safari 必须已退出)。"""
import json
import os
import plistlib
import sys
import time
import uuid
SAFARI_PLIST = os.path.expanduser("~/Library/Safari/Bookmarks.plist")
CHROME_BOOKMARKS = os.path.expanduser(
"~/Library/Application Support/Google/Chrome/Default/Bookmarks"
)
FOLDER_TITLE = "Chrome 书签 (jordansilly77@gmail.com)"
ROOT_TITLES = {
"bookmark_bar": "书签栏",
"other": "其他书签",
"synced": "移动设备书签",
}
def new_uuid():
return str(uuid.uuid4()).upper()
def chrome_leaves():
"""把 Chrome 书签摊平成 [(folder_path, name, url)]。"""
data = json.load(open(CHROME_BOOKMARKS, encoding="utf-8"))
out = []
def walk(node, path):
for child in node.get("children") or []:
if child.get("type") == "url":
out.append((path, child.get("name") or child.get("url"), child["url"]))
elif child.get("type") == "folder":
walk(child, path + [child.get("name") or "未命名"])
for key in ("bookmark_bar", "other", "synced"):
root = data["roots"].get(key)
if root:
walk(root, [ROOT_TITLES[key]])
return out
def make_leaf(name, url):
return {
"URIDictionary": {"title": name},
"URLString": url,
"WebBookmarkType": "WebBookmarkTypeLeaf",
"WebBookmarkUUID": new_uuid(),
}
def find_list(node, title):
if node.get("WebBookmarkType") == "WebBookmarkTypeList" and node.get("Title") == title:
return node
for child in node.get("Children", []):
found = find_list(child, title)
if found:
return found
return None
def existing_urls(node, acc):
if node.get("WebBookmarkType") == "WebBookmarkTypeLeaf":
acc.add(node.get("URLString"))
for child in node.get("Children", []):
existing_urls(child, acc)
return acc
def main():
if not os.path.exists(SAFARI_PLIST):
raise SystemExit("找不到 Safari Bookmarks.plist")
with open(SAFARI_PLIST, "rb") as fh:
bookmarks = plistlib.load(fh)
bar = find_list(bookmarks, "BookmarksBar")
if bar is None:
raise SystemExit("Safari 书签里没有 BookmarksBar 节点")
seen = existing_urls(bookmarks, set())
leaves = chrome_leaves()
# 按 Chrome 文件夹结构分组(去掉根节点那一层)
groups = {}
for path, name, url in leaves:
if url in seen:
continue
key = " / ".join(path[1:]) if len(path) > 1 else ""
groups.setdefault(key, []).append(make_leaf(name, url))
total_added = 0
children = []
for key in sorted(groups):
items = groups[key]
total_added += len(items)
if key:
children.append({
"Title": key,
"WebBookmarkType": "WebBookmarkTypeList",
"WebBookmarkUUID": new_uuid(),
"Children": items,
})
else:
children.extend(items)
if total_added == 0:
print(json.dumps({"added": 0, "note": "所有书签已存在"}, ensure_ascii=False))
return
folder = {
"Title": FOLDER_TITLE,
"WebBookmarkType": "WebBookmarkTypeList",
"WebBookmarkUUID": new_uuid(),
"Children": children,
}
bar.setdefault("Children", []).append(folder)
tmp = SAFARI_PLIST + ".tmp-merge"
with open(tmp, "wb") as fh:
plistlib.dump(bookmarks, fh, fmt=plistlib.FMT_BINARY)
os.replace(tmp, SAFARI_PLIST)
print(json.dumps({
"added": total_added,
"chrome_total": len(leaves),
"skipped_existing": len(leaves) - total_added,
"folder_title": FOLDER_TITLE,
"safari_plist": SAFARI_PLIST,
"written_at": time.strftime("%Y-%m-%d %H:%M:%S"),
}, ensure_ascii=False))
if __name__ == "__main__":
sys.exit(main())