π§ FULL WORKING PIPELINE (ColorNote β Joplin on Linux) β 2026-05-05
Note: Python script should be cross platform
Disclaimer: No guarantees are given, but I just wanted to leave some help as many people helped me in the past.
1) Install Required Tools
sudo apt update
sudo apt install default-jre python3 git jq
jq is optional but useful for JSON inspection.
2) Get the ColorNote Decryptor
git clone https://github.com/olejorgenb/ColorNote-backup-decryptor.git
cd ColorNote-backup-decryptor
3) Decrypt .backup β notes.json
java -cp lib/bcprov-jdk15on-154.jar:lib/bcpkix-jdk15on-154.jar:bin \
ColorNoteBackupDecrypt 0000 28 < colornote-20260504.backup > colornote-20260504.json
Change the date to whatever date came with your exported file: colornote-xxxxxxxx.backup
Make sure to use the same date in the decrypted JSON filename: colornote-xxxxxxxx.json
You can call the file whatever you want, but the date should be kept the same.
π Notes on parameters:
0000 = default PIN (if no password was set)
- Replace if user-defined PIN exists
28 = header offset (skip encrypted header)
- some backups may require
0
β οΈ fallback if no proper date is given:
ColorNoteBackupDecrypt 0000 0 < colornote-20260504.backup > colornotes.json
π 4) EXPORT DIRECTORY DATE RULE (UPDATED)
The export folder name uses the date extracted from the backup filename (or from the JSON filename, if user prefers).
Expected filename format:
colornote-YYYYMMDD.backup
Example:
colornote-20260504.backup
Behavior:
- If a valid
YYYYMMDD date exists in the filename β it is used for the export directory date
- If no valid date exists β the script falls back to the time of script execution
β οΈ IMPORTANT
Make sure your backup file follows the naming format above.
If the date is missing or invalid, the export will use the current execution date instead.
User must ensure the decrypted JSON file corresponds to the same date.
π§ͺ 5) Inspect Individual Fields (Optional Forensic Step)
OFFSET=$(grep -aob "Cashew roasted, no skin, unsalted" notes.json | head -n 1 | cut -d: -f1)
dd if=notes.json bs=1 skip=$((OFFSET-800)) count=1600 2>/dev/null | strings
Example:
{"_id":1368,"title":"Groceries","note":"[ ] Pine nuts 1/2 kg\n[ ] Cashew roasted...\n","encrypted":0,"modified_date":1775741055770,"active_state":0,"folder_id":0,"status":0,"space":0,"type":16,"color_index":3,"importance":0,"created_date":1775740543781,"uuid":"7451f2b6-55d0-4553-92bc-fa289f976476"}
π¬ Forensic Observation Note
Everything is contained with {} for each note, and contains all the required fields for that note.
After studying a bit of these, you will pick up the pattern that is used for saving information for archives and so on.
This is useful in case others want to build further and find other ways to restore and use the information contained.
π§ͺ Offset Insight
- OFFSET=28 β header exists
- OFFSET=0 β no header
π§ STRUCTURE MODEL (HIERARCHY MAP)
ColorNote Export Root
β
βββ Active
β βββ Default
β β βββ Notes
β β βββ Tasklists
β βββ Reminders
β βββ Notes
β βββ Tasklists
β
βββ Archive
β βββ Default
β β βββ Notes
β β βββ Tasklists
β βββ Reminders
β βββ Notes
β βββ Tasklists
β
βββ System
βββ Unknown
π§ DESIGN RULES
folder_id logic
0 β normal
16 β reminder
256 β system
- else β unknown
state logic
space == 0 β Active
space == 16 β Archive
checklist logic
6) Updated Python Export Script
"""
ColorNote β Joplin Import Script
Author: Ramaddan
Date: 2026-05-05
Version: 1.6
Description: Converts ColorNote JSON backups into Joplin-ready markdown notes
with FrontMatter. Supports emojis for colors, folder tagging, checklist conversion,
Arabic/Unicode-safe titles, UUID filenames, active/archive separation, and
dated export directories in the format: ColorNote Import (YYYY-MM-DD).
Note:
This script was developed with assistance from an AI system and manually reviewed/adjusted.
"""
import json
import os
import re
from datetime import datetime, timezone
# ---------------------------- User Settings ----------------------------
folder_name = "ColorNote Import"
folder_name_with_date = True
# The JSON file to process
notes_filename = "notes.json"
# Optional: original backup filename if available
backup_filename = "colornote-20260504.backup"
color_map = {
0: "",
1: "π₯",
2: "π§",
3: "π¨",
4: "π©",
5: "π¦",
6: "πͺ",
}
folder_map = {
0: "Default",
1: "Work",
2: "Personal",
}
# ------------------------ End User Settings ---------------------------
# ----------------------- Helper Functions -----------------------------
def extract_date_from_filename(filename):
"""
Extract YYYYMMDD from a filename like 'colornote-20260504.backup' or 'notes-20260504.json'.
Returns a string in YYYY-MM-DD format. If not found, returns None.
"""
match = re.search(r'(\d{4})(\d{2})(\d{2})', filename)
if match:
y, m, d = match.groups()
try:
return f"{y}-{m}-{d}"
except ValueError:
return None
return None
def convert_checklist(text):
lines = text.splitlines()
return "\n".join(
("- " + l.strip() if l.strip().startswith(("[ ]", "[x]", "[X]")) else l)
for l in lines
)
def timestamp_to_iso(ms):
if not ms:
return None
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc).isoformat()
def yaml_safe(s):
if not s:
return ""
return str(s).replace('"', '\\"').strip()
def classify_folder(folder_id):
if folder_id == 0:
return "normal"
elif folder_id == 16:
return "reminder"
elif folder_id == 256:
return "system"
else:
return "unknown"
# ----------------------- Determine export date -------------------------
date_from_notes = extract_date_from_filename(notes_filename)
date_from_backup = extract_date_from_filename(backup_filename)
export_date = date_from_notes or date_from_backup or datetime.now().strftime("%Y-%m-%d")
if folder_name_with_date:
outdir = f"{folder_name} ({export_date})"
else:
outdir = folder_name
active_dir = os.path.join(outdir, "Active")
archive_dir = os.path.join(outdir, "Archive")
os.makedirs(active_dir, exist_ok=True)
os.makedirs(archive_dir, exist_ok=True)
# ----------------------- LOAD NOTES JSON ------------------------------
with open(notes_filename, "rb") as f:
raw = f.read().decode("utf-8", errors="ignore")
decoder = json.JSONDecoder()
notes = []
i = 0
length = len(raw)
while i < length:
try:
obj, offset = decoder.raw_decode(raw[i:])
notes.append(obj)
i += offset
except json.JSONDecodeError:
i += 1
print(f"Parsed objects: {len(notes)}")
# ----------------------- OPTIONAL SYSTEM HANDLING -----------------------------
system_notes = []
for n in notes:
if not isinstance(n, dict):
continue
if classify_folder(n.get("folder_id", 0)) == "system":
system_notes.append(n)
if system_notes:
print("\n[!] System/internal notes detected:")
print(" These are NOT user-created notes.\n")
for n in system_notes:
print(f" - {n.get('title')} (folder_id={n.get('folder_id')})")
print("\nChoose how to handle system data:")
print(" [1] Keep")
print(" [2] Isolate")
print(" [3] Skip")
choice = input("\nEnter choice (1/2/3): ").strip()
keep_system = choice == "1"
isolate_system = choice == "2"
skip_system = choice == "3"
else:
keep_system = True
isolate_system = False
skip_system = False
# ----------------------- EXPORT NOTES -------------------------------
count = 1
total_notes = 0
active_notes = 0
archived_notes = 0
checklist_notes = 0
for n in notes:
if not isinstance(n, dict):
continue
total_notes += 1
folder_id = n.get("folder_id", 0)
note_class = classify_folder(folder_id)
title = (n.get("title") or "untitled").strip()
text = n.get("note") or ""
if not isinstance(text, str) or not text.strip():
continue
# ---------------- ARCHIVE STATE ----------------
is_archived = (n.get("space") == 16)
root_dir = archive_dir if is_archived else active_dir
# ---------------- CHECKLIST FLAG ----------------
is_checklist = (n.get("type") == 16)
# ---------------- GEOLOCATION (ADDED) ----------------
lat = n.get("latitude", 0)
lon = n.get("longitude", 0)
geo_tag = None
geo_link = None
if lat and lon and (lat != 0 or lon != 0):
geo_tag = "π location"
geo_link = f"https://www.google.com/maps?q={lat},{lon}"
# ---------------- ROUTING ----------------
if note_class == "normal":
base_dir = root_dir
folder_name_original = os.path.join(
folder_map.get(folder_id, "Default"),
"Tasklists" if is_checklist else "Notes"
)
elif note_class == "reminder":
base_dir = root_dir
folder_name_original = os.path.join(
"Reminders", "Tasklists" if is_checklist else "Notes"
)
elif note_class == "system":
if skip_system:
continue
base_dir = os.path.join(outdir, "System")
folder_name_original = ""
else:
if skip_system:
continue
base_dir = os.path.join(outdir, "Unknown")
folder_name_original = f"Folder_{folder_id}"
# ---------------- CHECKLIST CONVERSION ----------------
if is_checklist:
text = convert_checklist(text)
checklist_notes += 1
# ---------------- STATS ----------------
if is_archived:
archived_notes += 1
else:
active_notes += 1
# ---------------- PATH ----------------
folder_path = os.path.join(base_dir, folder_name_original)
os.makedirs(folder_path, exist_ok=True)
# ---------------- METADATA FLAGS ----------------
tags_extra = []
if n.get("status", 0) != 0:
tags_extra.append("π pinned")
if n.get("importance", 0) > 0:
tags_extra.append("β important")
if n.get("reminder_date", 0):
tags_extra.append("β° reminder")
if n.get("reminder_repeat", 0):
tags_extra.append("π recurring")
if geo_tag:
tags_extra.append(geo_tag)
# ---------------- FILE ID ----------------
color = n.get("color_index", 0)
emoji = color_map.get(color, "")
uid = n.get("uuid") or f"no_uuid_{count}"
filename = f"{uid}.md"
path = os.path.join(folder_path, filename)
if os.path.exists(path):
continue
# ---------------- FRONTMATTER ----------------
created_iso = timestamp_to_iso(n.get("created_date"))
modified_iso = timestamp_to_iso(n.get("modified_date"))
tags_list = list(filter(None, [emoji, folder_name_original])) + tags_extra
front_matter_lines = [
"---",
f'title: "{yaml_safe(title)}"',
]
if tags_list:
front_matter_lines.append("tags:")
for t in tags_list:
front_matter_lines.append(f' - "{yaml_safe(t)}"')
front_matter_lines.append(f'notebook: "{yaml_safe("Archive" if is_archived else "Active")}"')
if created_iso:
front_matter_lines.append(f'created: "{created_iso}"')
if modified_iso:
front_matter_lines.append(f'updated: "{modified_iso}"')
front_matter_lines.append("---")
front_matter_lines.append("")
content = "\n".join(front_matter_lines) + text.lstrip()
if geo_link:
content += f"\n\nMap: {geo_link}\n"
# ---------------- WRITE FILE ----------------
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(content)
# Update file timestamps to current time
os.utime(path, None)
count += 1
# ----------------------- FINAL OUTPUT -------------------------------
print(f"Import complete: {count - 1} new notes added")
print(f"Exported notes are in: {outdir}")
print("\n=== Export Summary ===")
print(f"Total parsed notes : {total_notes}")
print(f"Active notes : {active_notes}")
print(f"Archived notes : {archived_notes}")
print(f"Checklists : {checklist_notes}")
print(f"Written notes : {count - 1}")
π₯ FINAL IMPORT STEP
Import into Joplin using:
Import MD+FrontMatter (directory)
π§ FULL WORKING PIPELINE (ColorNote β Joplin on Linux) β 2026-05-05
Note: Python script should be cross platform
Disclaimer: No guarantees are given, but I just wanted to leave some help as many people helped me in the past.
1) Install Required Tools
jqis optional but useful for JSON inspection.2) Get the ColorNote Decryptor
git clone https://github.com/olejorgenb/ColorNote-backup-decryptor.git cd ColorNote-backup-decryptor3) Decrypt .backup β notes.json
Change the date to whatever date came with your exported file:
colornote-xxxxxxxx.backupMake sure to use the same date in the decrypted JSON filename:
colornote-xxxxxxxx.jsonYou can call the file whatever you want, but the date should be kept the same.
π Notes on parameters:
0000= default PIN (if no password was set)28= header offset (skip encrypted header)0π 4) EXPORT DIRECTORY DATE RULE (UPDATED)
The export folder name uses the date extracted from the backup filename (or from the JSON filename, if user prefers).
Expected filename format:
Example:
Behavior:
YYYYMMDDdate exists in the filename β it is used for the export directory dateMake sure your backup file follows the naming format above.
If the date is missing or invalid, the export will use the current execution date instead.
User must ensure the decrypted JSON file corresponds to the same date.
π§ͺ 5) Inspect Individual Fields (Optional Forensic Step)
Example:
{"_id":1368,"title":"Groceries","note":"[ ] Pine nuts 1/2 kg\n[ ] Cashew roasted...\n","encrypted":0,"modified_date":1775741055770,"active_state":0,"folder_id":0,"status":0,"space":0,"type":16,"color_index":3,"importance":0,"created_date":1775740543781,"uuid":"7451f2b6-55d0-4553-92bc-fa289f976476"}π¬ Forensic Observation Note
Everything is contained with
{}for each note, and contains all the required fields for that note.After studying a bit of these, you will pick up the pattern that is used for saving information for archives and so on.
This is useful in case others want to build further and find other ways to restore and use the information contained.
π§ͺ Offset Insight
π§ STRUCTURE MODEL (HIERARCHY MAP)
π§ DESIGN RULES
folder_id logic
0β normal16β reminder256β systemstate logic
space == 0β Activespace == 16β Archivechecklist logic
type == 16β checklist6) Updated Python Export Script
π₯ FINAL IMPORT STEP
Import into Joplin using: