macOS tools moving to my git
This commit is contained in:
311
arc-export/main.py
Executable file
311
arc-export/main.py
Executable file
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Colors:
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
UNDERLINE = "\033[4m"
|
||||
|
||||
BLACK = "\033[30m"
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
BLUE = "\033[34m"
|
||||
MAGENTA = "\033[35m"
|
||||
CYAN = "\033[36m"
|
||||
WHITE = "\033[37m"
|
||||
GREY = "\033[90m"
|
||||
|
||||
@staticmethod
|
||||
def Background(color: str) -> str:
|
||||
return color.replace("[3", "[4", 1)
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
time_format = f"{Colors.GREY}%(asctime)s{Colors.RESET}"
|
||||
FORMATS = {
|
||||
logging.DEBUG: f"{time_format} {Colors.BOLD}{Colors.CYAN}DEBG{Colors.RESET} %(message)s",
|
||||
logging.INFO: f"{time_format} {Colors.BOLD}{Colors.GREEN}INFO{Colors.RESET} %(message)s",
|
||||
logging.WARNING: f"{time_format} {Colors.BOLD}{Colors.YELLOW}WARN{Colors.RESET} %(message)s",
|
||||
logging.ERROR: f"{time_format} {Colors.BOLD}{Colors.RED}ERRR{Colors.RESET} %(message)s",
|
||||
logging.CRITICAL: f"{time_format} {Colors.BOLD}{Colors.Background(Colors.RED)}CRIT{Colors.RESET} %(message)s",
|
||||
}
|
||||
|
||||
def format(self, record: any) -> str:
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt, datefmt="%H:%M")
|
||||
return formatter.format(record)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="reads Arc Browser JSON data, converts it to HTML, and writes the output to a specified file."
|
||||
)
|
||||
parser.add_argument("-s", "--silent", action="store_true", help="silence output")
|
||||
parser.add_argument(
|
||||
"-o", "--output", type=Path, required=False, help="specify the output file path"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="enable verbose output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="store_true",
|
||||
help="print the git short hash and commit time",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.silent:
|
||||
logging.disable(logging.CRITICAL)
|
||||
else:
|
||||
setup_logging(args.verbose)
|
||||
|
||||
if args.version:
|
||||
commit_hash, commit_time = get_version()
|
||||
if commit_hash is None or commit_time is None:
|
||||
logging.critical("Could not fetch Git metadata.")
|
||||
return
|
||||
print(
|
||||
f"{Colors.BOLD}GIT TIME{Colors.RESET} | {Colors.GREEN}{commit_time.strftime('%Y-%m-%d')}{Colors.RESET} [{Colors.YELLOW}{int(commit_time.timestamp())}{Colors.RESET}]"
|
||||
)
|
||||
print(
|
||||
f"{Colors.BOLD}GIT HASH{Colors.RESET} | {Colors.MAGENTA}{commit_hash}{Colors.RESET}"
|
||||
)
|
||||
return
|
||||
|
||||
data: dict = read_json()
|
||||
html: str = convert_json_to_html(data)
|
||||
write_html(html, args.output)
|
||||
logging.info("Done!")
|
||||
|
||||
|
||||
def setup_logging(is_verbose: bool) -> None:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(CustomFormatter())
|
||||
logging.basicConfig(level=logging.DEBUG, handlers=[handler])
|
||||
|
||||
if is_verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
|
||||
def get_version() -> tuple[str, datetime]:
|
||||
try:
|
||||
commit_hash: str = (
|
||||
subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
commit_time_str: str = (
|
||||
subprocess.check_output(["git", "log", "-1", "--format=%ct"])
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
commit_time = datetime.fromtimestamp(int(commit_time_str))
|
||||
except Exception:
|
||||
commit_hash = None
|
||||
commit_time = None
|
||||
|
||||
return commit_hash, commit_time
|
||||
|
||||
|
||||
def read_json() -> dict:
|
||||
logging.info("Reading JSON...")
|
||||
|
||||
filename: Path = Path("StorableSidebar.json")
|
||||
if os.name == "nt":
|
||||
arc_root_parent_path: Path = Path(
|
||||
os.path.expanduser(r"~\AppData\Local\Packages")
|
||||
)
|
||||
arc_root_paths: list[Path] = [
|
||||
f
|
||||
for f in arc_root_parent_path.glob("*")
|
||||
if f.name.startswith("TheBrowserCompany.Arc")
|
||||
]
|
||||
if len(arc_root_paths) != 1:
|
||||
raise FileNotFoundError
|
||||
|
||||
library_path: Path = Path(
|
||||
arc_root_paths[0].joinpath(r"LocalCache\Local\Arc")
|
||||
).joinpath(filename)
|
||||
|
||||
else:
|
||||
library_path: Path = Path(
|
||||
os.path.expanduser("~/Library/Application Support/Arc/")
|
||||
).joinpath(filename)
|
||||
|
||||
data: dict = {}
|
||||
|
||||
if filename.exists():
|
||||
with filename.open("r", encoding="utf-8") as f:
|
||||
logging.debug(f"Found {filename} in current directory.")
|
||||
data = json.load(f)
|
||||
|
||||
elif library_path.exists():
|
||||
with library_path.open("r", encoding="utf-8") as f:
|
||||
logging.debug(f"Found {filename} in Library directory.")
|
||||
data = json.load(f)
|
||||
|
||||
else:
|
||||
logging.critical(
|
||||
'> File not found. Look for the "StorableSidebar.json" '
|
||||
' file within the "~/Library/Application Support/Arc/" folder.'
|
||||
)
|
||||
raise FileNotFoundError
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def convert_json_to_html(json_data: dict) -> str:
|
||||
containers: list = json_data["sidebar"]["containers"]
|
||||
try:
|
||||
target: int = next(i + 1 for i, c in enumerate(containers) if "global" in c)
|
||||
except StopIteration:
|
||||
raise ValueError("No container with 'global' found in the sidebar data")
|
||||
|
||||
spaces: dict = get_spaces(json_data["sidebar"]["containers"][target]["spaces"])
|
||||
items: list = json_data["sidebar"]["containers"][target]["items"]
|
||||
|
||||
bookmarks: dict = convert_to_bookmarks(spaces, items)
|
||||
html_content: str = convert_bookmarks_to_html(bookmarks)
|
||||
|
||||
return html_content
|
||||
|
||||
|
||||
def get_spaces(spaces: list) -> dict:
|
||||
logging.info("Getting spaces...")
|
||||
|
||||
spaces_names: dict = {"pinned": {}, "unpinned": {}}
|
||||
spaces_count: int = 0
|
||||
n: int = 1
|
||||
|
||||
for space in spaces:
|
||||
if "title" in space:
|
||||
title: str = space["title"]
|
||||
else:
|
||||
title: str = "Space " + str(n)
|
||||
n += 1
|
||||
|
||||
# TODO: Find a better way to determine if a space is pinned or not
|
||||
if isinstance(space, dict):
|
||||
containers: list = space["newContainerIDs"]
|
||||
|
||||
for i in range(len(containers)):
|
||||
if isinstance(containers[i], dict):
|
||||
if "pinned" in containers[i]:
|
||||
spaces_names["pinned"][str(containers[i + 1])]: str = title
|
||||
elif "unpinned" in containers[i]:
|
||||
spaces_names["unpinned"][str(containers[i + 1])]: str = title
|
||||
|
||||
spaces_count += 1
|
||||
|
||||
logging.debug(f"Found {spaces_count} spaces.")
|
||||
|
||||
return spaces_names
|
||||
|
||||
|
||||
def convert_to_bookmarks(spaces: dict, items: list) -> dict:
|
||||
logging.info("Converting to bookmarks...")
|
||||
|
||||
bookmarks: dict = {"bookmarks": []}
|
||||
bookmarks_count: int = 0
|
||||
item_dict: dict = {item["id"]: item for item in items if isinstance(item, dict)}
|
||||
|
||||
def recurse_into_children(parent_id: str) -> list:
|
||||
nonlocal bookmarks_count
|
||||
children: list = []
|
||||
for item_id, item in item_dict.items():
|
||||
if item.get("parentID") == parent_id:
|
||||
if "data" in item and "tab" in item["data"]:
|
||||
children.append(
|
||||
{
|
||||
"title": item.get("title", None)
|
||||
or item["data"]["tab"].get("savedTitle", ""),
|
||||
"type": "bookmark",
|
||||
"url": item["data"]["tab"].get("savedURL", ""),
|
||||
}
|
||||
)
|
||||
bookmarks_count += 1
|
||||
elif "title" in item:
|
||||
child_folder: dict = {
|
||||
"title": item["title"],
|
||||
"type": "folder",
|
||||
"children": recurse_into_children(item_id),
|
||||
}
|
||||
children.append(child_folder)
|
||||
return children
|
||||
|
||||
for space_id, space_name in spaces["pinned"].items():
|
||||
space_folder: dict = {
|
||||
"title": space_name,
|
||||
"type": "folder",
|
||||
"children": recurse_into_children(space_id),
|
||||
}
|
||||
bookmarks["bookmarks"].append(space_folder)
|
||||
|
||||
logging.debug(f"Found {bookmarks_count} bookmarks.")
|
||||
|
||||
return bookmarks
|
||||
|
||||
|
||||
def convert_bookmarks_to_html(bookmarks: dict) -> str:
|
||||
logging.info("Converting bookmarks to HTML...")
|
||||
|
||||
html_str: str = """<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>"""
|
||||
|
||||
def traverse_dict(d: dict, html_str: str, level: int) -> str:
|
||||
indent: str = "\t" * level
|
||||
for item in d:
|
||||
if item["type"] == "folder":
|
||||
html_str += f'\n{indent}<DT><H3>{item["title"]}</H3>'
|
||||
html_str += f"\n{indent}<DL><p>"
|
||||
html_str = traverse_dict(item["children"], html_str, level + 1)
|
||||
html_str += f"\n{indent}</DL><p>"
|
||||
elif item["type"] == "bookmark":
|
||||
html_str += f'\n{indent}<DT><A HREF="{item["url"]}">{item["title"]}</A>'
|
||||
return html_str
|
||||
|
||||
html_str = traverse_dict(bookmarks["bookmarks"], html_str, 1)
|
||||
html_str += "\n</DL><p>"
|
||||
|
||||
logging.debug("HTML converted.")
|
||||
|
||||
return html_str
|
||||
|
||||
|
||||
def write_html(html_content: str, output: Path = None) -> None:
|
||||
logging.info("Writing HTML...")
|
||||
|
||||
if output is not None:
|
||||
output_file: Path = output
|
||||
else:
|
||||
current_date: str = datetime.now().strftime("%Y_%m_%d")
|
||||
output_file: Path = Path("arc_bookmarks_" + current_date).with_suffix(".html")
|
||||
|
||||
with output_file.open("w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
|
||||
logging.debug(f"HTML written to {output_file}.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user