Created
December 5, 2018 02:55
-
-
Save dunhamsteve/a4b1198a9f07d5425e6e5022188855d5 to your computer and use it in GitHub Desktop.
reads cached data from Notion.app's localStorage, collects incomplete todo items, and writes them to todo.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 | |
# This is written for OSX, python3, and the current (2018-12-04) version of the Notion app | |
# It writes incomplete todo items, found in Notion's localStorage, to a "todo.html" file, | |
# with links back to the Notion app | |
import os | |
import sqlite3 | |
import json | |
from html import escape | |
from collections import defaultdict | |
# load cached blocks from Notion desktop's localStorage | |
blocks = {} | |
path = os.path.expanduser( | |
"~/Library/Application Support/Notion/Local Storage/https_www.notion.so_0.localstorage") | |
c = sqlite3.Connection(path) | |
for k, v in c.execute("select key,value from itemtable where key like 'LRU:LocalRecordStore3:%'"): | |
_, _, uuid, table = k.split(':') | |
data = json.loads(v.decode('utf16')).get('value', {}).get('value') | |
if data: | |
blocks[uuid] = data | |
def page_name(id): | |
"fetch name of page as text or 'Missinge Page'" | |
page = blocks.get(id, {}) | |
if not page: | |
return "Missing Page" | |
return prop(page, 'title') | |
tags = {'b': 'b', 'i': 'em', 'c': 'code'} | |
def rich_prop(block, key): | |
"renders rich property to html" | |
parts = block.get('properties', {}).get(key, []) | |
rval = [] | |
for part in parts: | |
text = part[0] | |
if len(part) > 1: | |
for seg in part[1]: | |
if seg[0] == 'p': text = page_name(seg[1]) | |
if seg[0] == 'a': text = ['a', {'href': seg[1]}, text] | |
tag = tags.get(seg[0]) | |
if tag: | |
text = [tag, text] | |
rval.append(text) | |
return rval | |
def prop(block, key): | |
"returns text from a property (sans page names)" | |
if block is not None: | |
parts = block.get('properties', {}).get(key, []) | |
return ''.join(part[0] for part in parts) | |
def get_page(block): | |
"return id of containing page or None" | |
while block != None: | |
if block.get('type') == 'page': | |
return block.get('id') | |
block = blocks.get(block.get('parent_id')) | |
def link(pid,bid=None): | |
"link to block in page" | |
rval = 'notion://-/'+pid.replace('-','') | |
if bid: | |
return rval + '#' + bid.replace('-','') | |
return rval | |
def todo(b, pid): | |
"render todo item" | |
bid = b.get('id') | |
title = rich_prop(b, 'title') | |
return ['div', ['a', {'href': link(pid,bid)},"\u2610"], *title] | |
def render_html(node): | |
"turn a pile of lists and dicts into html" | |
tag, *rest = node | |
attr = [] | |
body = [] | |
for x in rest: | |
if isinstance(x, dict): | |
for k, v in x.items(): | |
attr.append(f' {k}="{escape(v)}"') | |
elif isinstance(x, list): | |
body.append(render_html(x)) | |
else: | |
body.append(escape(str(x))) | |
return ''.join(['<', tag, *attr, '>', *body, '</', tag, '>']) | |
# collect todo items by page. | |
pages = defaultdict(list) | |
for id, b in sorted(blocks.items()): | |
if b.get('type') == 'to_do': | |
checked = prop(b, 'checked') == 'Yes' | |
if not checked: | |
pid = get_page(b) | |
pages[pid].append(b) | |
# render todo items | |
div = ['div', {'class': 'content'}] | |
# for now, sort by id to make order stable | |
for pid, items in sorted(pages.items()): | |
title = prop(blocks.get(pid), 'title') | |
items = [todo(item, pid) for item in items] | |
div.append(['h3', title]) | |
div.append(['div', *items]) | |
css = ''' | |
body { font-family: sans-serif; } | |
div.content { width: 800px; margin: auto; } | |
a { text-decoration: none; color: #555; } | |
''' | |
out = render_html(['html', | |
['head', ['style', css]], | |
['body', div]]) | |
print(out, file=open('todo.html', 'w')) | |
print('wrote todo.html') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment