Created
May 25, 2026 20:37
-
-
Save cfm/ecf2a755989ff2d0d33436f54959eaa0 to your computer and use it in GitHub Desktop.
courtesy of Claude
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 | |
| """Export open Safari tabs from SafariTabs.db to a Netscape-format bookmarks.html.""" | |
| import argparse | |
| import html | |
| import sqlite3 | |
| import sys | |
| from pathlib import Path | |
| BOOKMARKS_HEADER = """\ | |
| <!DOCTYPE NETSCAPE-Bookmark-file-1> | |
| <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> | |
| <TITLE>Bookmarks</TITLE> | |
| <H1>Bookmarks</H1> | |
| <DL><p> | |
| """ | |
| BOOKMARKS_FOOTER = "</DL><p>\n" | |
| def get_tabs(db_path: str) -> list[dict]: | |
| """Return all open tabs grouped by window, in tab order.""" | |
| con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) | |
| con.row_factory = sqlite3.Row | |
| cur = con.cursor() | |
| # Each window has one or more tab groups; each tab group's children are tabs. | |
| cur.execute(""" | |
| SELECT | |
| w.id AS window_id, | |
| wtg.tab_group_id, | |
| tg.title AS group_title, | |
| b.title AS tab_title, | |
| b.url | |
| FROM windows w | |
| JOIN windows_tab_groups wtg ON wtg.window_id = w.id | |
| JOIN bookmarks tg ON tg.id = wtg.tab_group_id | |
| JOIN bookmarks b ON b.parent = tg.id | |
| WHERE w.date_closed IS NULL | |
| AND b.deleted = 0 | |
| AND b.url IS NOT NULL | |
| AND b.url != '' | |
| ORDER BY w.id, tg.order_index, b.order_index | |
| """) | |
| rows = cur.fetchall() | |
| con.close() | |
| return [dict(r) for r in rows] | |
| def build_html(tabs: list[dict]) -> str: | |
| if not tabs: | |
| return BOOKMARKS_HEADER + BOOKMARKS_FOOTER | |
| lines = [BOOKMARKS_HEADER] | |
| # Group by (window_id, tab_group_id) to emit one folder per tab group. | |
| seen: dict[tuple, list] = {} | |
| for tab in tabs: | |
| key = (tab["window_id"], tab["tab_group_id"]) | |
| seen.setdefault(key, []).append(tab) | |
| for (window_id, _), group_tabs in seen.items(): | |
| group_title = group_tabs[0]["group_title"] or f"Window {window_id}" | |
| lines.append(f' <DT><H3>{html.escape(group_title)}</H3>\n <DL><p>\n') | |
| for tab in group_tabs: | |
| title = html.escape(tab["tab_title"] or tab["url"]) | |
| url = html.escape(tab["url"]) | |
| lines.append(f' <DT><A HREF="{url}">{title}</A>\n') | |
| lines.append(' </DL><p>\n') | |
| lines.append(BOOKMARKS_FOOTER) | |
| return "".join(lines) | |
| def main(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("db", nargs="?", default="SafariTabs.db", | |
| help="path to SafariTabs.db (default: SafariTabs.db)") | |
| parser.add_argument("out", nargs="?", default="bookmarks.html", | |
| help="output file (default: bookmarks.html)") | |
| args = parser.parse_args() | |
| db_path = Path(args.db) | |
| if not db_path.exists(): | |
| sys.exit(f"error: {db_path} not found") | |
| tabs = get_tabs(str(db_path)) | |
| if not tabs: | |
| print("No open tabs found.", file=sys.stderr) | |
| html_out = build_html(tabs) | |
| out_path = Path(args.out) | |
| out_path.write_text(html_out, encoding="utf-8") | |
| print(f"Wrote {len(tabs)} tabs to {out_path}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment