Skip to content

# 🧭 FULL WORKING PIPELINE (ColorNote β†’ Joplin on Linux) β€” 2026-05-05Β #17

Description

@Ramaddan

🧭 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

  • type == 16 β†’ checklist

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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions