← 返回会话

merge_into_safari.py

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())