mirror of
https://expo.survex.com/repositories/expoweb/.git/
synced 2025-12-08 14:54:28 +00:00
302 lines
11 KiB
Python
302 lines
11 KiB
Python
import os
|
|
import json
|
|
|
|
""" This was written almost entirely by Google Gemini 2.5 (apps and code) on 18 Sept. 2025
|
|
|
|
Google Takeout is accessible from the browser when logged in to your Google account.
|
|
It can be set up to export dozens of different types of data. Here we only export "Keep"
|
|
data.
|
|
After running this, move the generated file to expoweb/handbook/troggle
|
|
and git add/commit/push
|
|
Philip Sargent
|
|
"""
|
|
# --- CONFIGURATION ---
|
|
# 📌 IMPORTANT: Replace this with the actual path to your Google Keep Takeout folder.
|
|
TAKEOUT_DIRECTORY = 'Takeout/Keep' # Example for Linux/macOS
|
|
#
|
|
|
|
def process_keep_files_to_dict(directory_path):
|
|
"""
|
|
Scans a directory for Google Keep JSON files, filters for a specific
|
|
label, prints the content, and returns the data as a list of dictionaries.
|
|
|
|
Args:
|
|
directory_path (str): The path to the folder containing the JSON files.
|
|
|
|
Returns:
|
|
list: A list of dictionaries, where each dictionary represents a note.
|
|
"""
|
|
print(f"📁 Processing files in: {directory_path}\n")
|
|
|
|
# This list will store the dictionary for each matching note
|
|
extracted_notes = []
|
|
|
|
if not os.path.isdir(directory_path):
|
|
print(f"❌ Error: Directory not found at '{directory_path}'.")
|
|
print("Please update the TAKEOUT_DIRECTORY variable with the correct path.")
|
|
return extracted_notes
|
|
|
|
# Iterate over every file in the specified directory
|
|
for filename in os.listdir(directory_path):
|
|
if filename.endswith('.json'):
|
|
file_path = os.path.join(directory_path, filename)
|
|
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
# --- Label Check ---
|
|
has_expo_label = False
|
|
if 'labels' in data:
|
|
if any(label.get('name', '').lower() == 'expo' for label in data['labels']):
|
|
has_expo_label = True
|
|
|
|
if not has_expo_label:
|
|
continue # Skip this file
|
|
|
|
# --- Data Extraction & Storage ---
|
|
|
|
# Create a dictionary to hold the current note's data
|
|
current_note = {
|
|
'title': data.get('title') or 'Untitled Note',
|
|
'source_file': filename,
|
|
'content_type': None,
|
|
'color': data.get('color', 'DEFAULT'),
|
|
'content': None
|
|
}
|
|
|
|
# As before, print to the console for immediate feedback
|
|
print("-" * 40)
|
|
print(f"✅ Title: {current_note['title']}")
|
|
|
|
# Handle text notes
|
|
if 'textContent' in data:
|
|
content = data['textContent']
|
|
current_note['content_type'] = 'text'
|
|
current_note['content'] = content
|
|
print(content)
|
|
|
|
# Handle list notes
|
|
elif 'listContent' in data:
|
|
current_note['content_type'] = 'list'
|
|
list_items = []
|
|
checked_items = []
|
|
# print("Tasks:")
|
|
for item in data['listContent']:
|
|
# Create a clean dictionary for the list item
|
|
item_data = {
|
|
'text': item.get('text', ''),
|
|
'is_checked': item.get('isChecked', False)
|
|
}
|
|
list_items.append(item_data)
|
|
if item_data['is_checked']:
|
|
checked_items.append(item_data)
|
|
|
|
# Print the list item to the console
|
|
status = 'x' if item_data['is_checked'] else ' '
|
|
# print(f" [{status}] {item_data['text']}")
|
|
print(f"Tasks: {len(list_items)} of which {len(checked_items)} are done.")
|
|
current_note['content'] = list_items
|
|
|
|
# Add the completed dictionary for this note to our main list
|
|
extracted_notes.append(current_note)
|
|
|
|
# print("-" * 40 + "\n")
|
|
|
|
except Exception as e:
|
|
print(f"❗️ An unexpected error occurred with file {filename}: {e}")
|
|
|
|
if not extracted_notes:
|
|
print("No notes with the 'EXPO' label were found in the directory.")
|
|
|
|
return extracted_notes
|
|
|
|
import html
|
|
from datetime import datetime
|
|
|
|
def write_html_output(notes_data, filename="keep_export.html"):
|
|
"""
|
|
Generates an HTML file from the extracted Keep notes data.
|
|
|
|
Args:
|
|
notes_data (list): A list of note dictionaries.
|
|
filename (str): The name of the output HTML file.
|
|
"""
|
|
|
|
# These are the standard Google Keep colors.
|
|
color_map = {
|
|
"DEFAULT": "#ffffff",
|
|
"RED": "#f28b82",
|
|
"ORANGE": "#fbbc04",
|
|
"YELLOW": "#fff475",
|
|
"GREEN": "#ccff90",
|
|
"TEAL": "#a7ffeb",
|
|
"BLUE": "#cbf0f8",
|
|
"DARK_BLUE": "#aecbfa",
|
|
"PURPLE": "#d7aefb",
|
|
"PINK": "#fdcfe8",
|
|
"BROWN": "#e6c9a8",
|
|
"GRAY": "#e8eaed",
|
|
}
|
|
|
|
# --- HTML Head and CSS Styling ---
|
|
# The CSS is embedded directly in the HTML file for simplicity.
|
|
html_head = f"""
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Google Keep Export</title>
|
|
<style>
|
|
body {{
|
|
font-family: 'Google Sans', Roboto, Arial, sans-serif;
|
|
background-color: #f1f3f4;
|
|
margin: 0;
|
|
padding: 24px;
|
|
}}
|
|
.container {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 16px;
|
|
}}
|
|
.note-card {{
|
|
background-color: #ffffff;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
|
word-wrap: break-word; /* Prevents long text from overflowing */
|
|
}}
|
|
.note-title {{
|
|
font-size: 1.1em;
|
|
font-weight: 500;
|
|
margin: 0 0 12px 0;
|
|
}}
|
|
.note-content p {{
|
|
margin: 0;
|
|
white-space: pre-wrap; /* Respects newlines in text notes */
|
|
}}
|
|
.task-list {{
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}}
|
|
.task-item {{
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}}
|
|
.task-item input[type="checkbox"] {{
|
|
margin-right: 10px;
|
|
}}
|
|
.completed-item-text {{
|
|
text-decoration: line-through;
|
|
color: #5f6368;
|
|
}}
|
|
.completed-items {{
|
|
margin-top: 16px;
|
|
border-top: 1px solid #e0e0e0;
|
|
padding-top: 12px;
|
|
}}
|
|
.completed-items summary {{
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
color: #3c4043;
|
|
}}
|
|
</style>
|
|
</head>
|
|
"""
|
|
|
|
# --- HTML Body Generation ---
|
|
html_body_content = ""
|
|
for note in notes_data:
|
|
# Sanitize title to prevent HTML injection issues
|
|
title = html.escape(note['title'])
|
|
|
|
# Start building the content for this specific note
|
|
note_content_html = ""
|
|
|
|
# MODIFICATION 2: Get the color and apply it as an inline style
|
|
note_color_name = note.get('color', 'DEFAULT')
|
|
# Safely get the color from the map, falling back to the default white
|
|
bg_color = color_map.get(note_color_name, color_map['DEFAULT'])
|
|
|
|
# Handle text notes
|
|
if note['content_type'] == 'text':
|
|
# Sanitize text content and replace newlines with <br> tags
|
|
text = html.escape(note['content'])
|
|
note_content_html = f'<div class="note-content"><p>{text}</p></div>'
|
|
|
|
# Handle list notes
|
|
elif note['content_type'] == 'list':
|
|
unchecked_items = []
|
|
checked_items = []
|
|
|
|
for item in note['content']:
|
|
# Sanitize list item text
|
|
item_text = html.escape(item['text'])
|
|
if item['is_checked']:
|
|
checked_items.append(
|
|
f'<li class="task-item"><input type="checkbox" disabled checked><span class="completed-item-text">{item_text}</span></li>'
|
|
)
|
|
else:
|
|
unchecked_items.append(
|
|
f'<li class="task-item"><input type="checkbox" disabled><span>{item_text}</span></li>'
|
|
)
|
|
|
|
# Combine unchecked items
|
|
note_content_html += '<ul class="task-list">' + "".join(unchecked_items) + '</ul>'
|
|
|
|
# Add checked items inside a collapsible <details> element
|
|
if checked_items:
|
|
item_count = len(checked_items)
|
|
plural_s = 's' if item_count > 1 else ''
|
|
note_content_html += f"""
|
|
<details class="completed-items">
|
|
<summary>{item_count} completed item{plural_s}</summary>
|
|
<ul class="task-list">{"".join(checked_items)}</ul>
|
|
</details>
|
|
"""
|
|
|
|
# Combine title and content into a single note card
|
|
html_body_content += f"""
|
|
<div class="note-card" style="background-color: {bg_color};">
|
|
<h2 class="note-title">{title}</h2>
|
|
{note_content_html}
|
|
</div>
|
|
"""
|
|
# --- Final HTML Assembly ---
|
|
full_html = f"""
|
|
{html_head}
|
|
<body>
|
|
<h1>Troggle coding and design to-do lists</h1>
|
|
<p>(Exported from Philip Sargent's Google Keep notes on {datetime.now().strftime('%Y-%m-%d %H:%M')})
|
|
<p>Contact Philip if you wish to share these lists on your Google Keep account.
|
|
<div class="container">
|
|
{html_body_content}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# --- Writing to File ---
|
|
try:
|
|
with open(filename, 'w', encoding='utf-8') as f:
|
|
f.write(full_html)
|
|
print(f"\n🎉 Successfully wrote {len(notes_data)} notes to '{filename}'")
|
|
except Exception as e:
|
|
print(f"❗️ An error occurred while writing the file: {e}")
|
|
|
|
# --- Run the program ---
|
|
if __name__ == "__main__":
|
|
all_notes_data = process_keep_files_to_dict(TAKEOUT_DIRECTORY)
|
|
|
|
if all_notes_data:
|
|
print("\n" + "="*50)
|
|
print("🎉 Successfully processed all matching files.")
|
|
print(f"Total notes extracted: {len(all_notes_data)}")
|
|
# print("Data has been stored in the 'all_notes_data' variable.")
|
|
|
|
print("="*50)
|
|
write_html_output(all_notes_data) |