Last active
May 30, 2026 22:14
-
-
Save medhatdawoud/04e429a4b0d618a480a91985a571cb99 to your computer and use it in GitHub Desktop.
Convert Arc browser spaces to Chrome bookmark folders (with pinned tabs). Run with --html flag to generate importable HTML.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Convert Arc browser spaces to Chrome bookmark folders. | |
| Each space becomes a bookmark folder; pinned tabs become bookmarks inside. | |
| Run before launching Chrome for the first time on a new machine. | |
| """ | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import time | |
| from pathlib import Path | |
| ARC_DATA = Path.home() / "Library/Application Support/Arc/StorableSidebar.json" | |
| CHROME_DIR = Path.home() / "Library/Application Support/Google/Chrome/Default" | |
| CHROME_BKM = CHROME_DIR / "Bookmarks" | |
| def clean_url(url): | |
| return url.replace("\\/", "/") | |
| SKIP_SPACE_IDS = {"thebrowser.company.defaultPersonalSpaceID"} | |
| def extract_spaces(d): | |
| sync = d["firebaseSyncState"]["syncData"] | |
| # Build item lookup: id -> value dict | |
| items = sync["items"] | |
| lookup = {} | |
| i = 0 | |
| while i < len(items) - 1: | |
| if isinstance(items[i], str) and isinstance(items[i+1], dict): | |
| val = items[i+1].get("value", items[i+1]) | |
| lookup[items[i]] = items[i+1] | |
| i += 2 if (isinstance(items[i], str) and i+1 < len(items) and isinstance(items[i+1], dict)) else 1 | |
| # Parse space models | |
| space_models = sync["spaceModels"] | |
| spaces = [] | |
| i = 0 | |
| while i < len(space_models) - 1: | |
| if isinstance(space_models[i], str) and isinstance(space_models[i+1], dict): | |
| sid = space_models[i] | |
| data = space_models[i+1] | |
| if sid not in SKIP_SPACE_IDS: | |
| raw = json.dumps(data) | |
| tm = re.search(r'"title"\s*:\s*"([^"]+)"', raw) | |
| title = tm.group(1) if tm else sid[:8] | |
| # Find pinned container ID | |
| val = data.get("value", data) | |
| pinned_cid = None | |
| for cid in val.get("containerIDs", []): | |
| if isinstance(cid, str) and len(cid) == 36: | |
| # peek at next element to see if it's the pinned one | |
| pass | |
| new_cids = val.get("newContainerIDs", []) | |
| for j, c in enumerate(new_cids): | |
| if isinstance(c, dict) and "pinned" in c and j+1 < len(new_cids): | |
| pinned_cid = new_cids[j+1] if isinstance(new_cids[j+1], str) else None | |
| break | |
| # Collect pinned tab URLs | |
| bookmarks = [] | |
| if pinned_cid and pinned_cid in lookup: | |
| container = lookup[pinned_cid] | |
| cv = container.get("value", container) | |
| for child_id in cv.get("childrenIds", []): | |
| if child_id in lookup: | |
| tab_raw = json.dumps(lookup[child_id]) | |
| url_m = re.search(r'"savedURL"\s*:\s*"([^"]+)"', tab_raw) | |
| ttl_m = re.search(r'"savedTitle"\s*:\s*"([^"]+)"', tab_raw) | |
| if url_m: | |
| bookmarks.append({ | |
| "url": clean_url(url_m.group(1)), | |
| "title": ttl_m.group(1) if ttl_m else clean_url(url_m.group(1)) | |
| }) | |
| spaces.append({"title": title, "bookmarks": bookmarks}) | |
| i += 2 | |
| else: | |
| i += 1 | |
| return spaces | |
| def make_chrome_bookmarks(spaces): | |
| ts = str(int(time.time() * 1000000 + 11644473600000000)) | |
| uid = [10] | |
| def next_id(): | |
| uid[0] += 1 | |
| return str(uid[0]) | |
| def make_folder(name, children): | |
| return {"children": children, "date_added": ts, "date_modified": ts, | |
| "id": next_id(), "name": name, "type": "folder"} | |
| def make_url(title, url): | |
| return {"date_added": ts, "id": next_id(), "name": title, "type": "url", "url": url} | |
| folders = [] | |
| for space in spaces: | |
| children = [make_url(b["title"], b["url"]) for b in space["bookmarks"]] | |
| folders.append(make_folder(space["title"], children)) | |
| arc_folder = make_folder("Arc Spaces", folders) | |
| return { | |
| "checksum": "", | |
| "roots": { | |
| "bookmark_bar": make_folder("Bookmarks bar", [arc_folder]), | |
| "other": make_folder("Other bookmarks", []), | |
| "synced": make_folder("Mobile bookmarks", []) | |
| }, | |
| "version": 1 | |
| } | |
| def make_html(spaces): | |
| lines = [ | |
| "<!DOCTYPE NETSCAPE-Bookmark-file-1>", | |
| "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">", | |
| "<TITLE>Bookmarks</TITLE>", | |
| "<H1>Bookmarks</H1>", | |
| "<DL><p>", | |
| " <DT><H3>Arc Spaces</H3>", | |
| " <DL><p>", | |
| ] | |
| for space in spaces: | |
| lines.append(f" <DT><H3>{space['title']}</H3>") | |
| lines.append(" <DL><p>") | |
| for b in space["bookmarks"]: | |
| title = b["title"].replace("&", "&").replace("<", "<").replace(">", ">") | |
| url = b["url"].replace("&", "&") | |
| lines.append(f' <DT><A HREF="{url}">{title}</A>') | |
| lines.append(" </DL><p>") | |
| lines += [" </DL><p>", "</DL>"] | |
| return "\n".join(lines) | |
| def main(): | |
| if not ARC_DATA.exists(): | |
| print(f"Arc data not found at {ARC_DATA}") | |
| print("Run this script on a machine with Arc installed.") | |
| sys.exit(1) | |
| html_mode = "--html" in sys.argv | |
| args = [a for a in sys.argv[1:] if not a.startswith("--")] | |
| print("Reading Arc spaces...") | |
| with open(ARC_DATA) as f: | |
| d = json.load(f) | |
| spaces = extract_spaces(d) | |
| print(f"Found {len(spaces)} active spaces:") | |
| for s in spaces: | |
| print(f" {s['title']} ({len(s['bookmarks'])} pinned tabs)") | |
| if html_mode: | |
| out = Path(args[0]) if args else Path("/tmp/arc_spaces.html") | |
| out.parent.mkdir(parents=True, exist_ok=True) | |
| out.write_text(make_html(spaces)) | |
| print(f"\nHTML bookmarks written to: {out}") | |
| print("Import via Chrome: chrome://bookmarks → ⋮ menu → Import bookmarks") | |
| os.system(f'open "{out}"') | |
| else: | |
| out = Path(args[0]) if args else Path("/tmp/chrome_bookmarks_from_arc.json") | |
| out.parent.mkdir(parents=True, exist_ok=True) | |
| with open(out, "w") as f: | |
| json.dump(make_chrome_bookmarks(spaces), f, indent=2) | |
| print(f"\nJSON bookmarks written to: {out}") | |
| if __name__ == "__main__": | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
From the gist directly (no clone needed):
curl -sL https://gist.github.com/medhatdawoud/04e429a4b0d618a480a91985a571cb99/raw | python3 - --html ~/arc_spaces.htmlThen in Chrome: