Skip to content

Instantly share code, notes, and snippets.

@aliva
Last active September 12, 2025 14:40
Show Gist options
  • Save aliva/7a79ab8adcbc2f26349ad16775ed70ef to your computer and use it in GitHub Desktop.
Save aliva/7a79ab8adcbc2f26349ad16775ed70ef to your computer and use it in GitHub Desktop.
Create markdown files based on your ticktick notes
"""
Create markdown files based on your ticktick notes
You need to export your data from ticktick and use as input:
* Export from: Setting -> Account -> Generate Backup
Usage:
> python main.py -f ticktick.csv -d output_dir/ [--flatten]
"""
from argparse import ArgumentParser, FileType
from csv import DictReader
from io import StringIO
from pathlib import Path
import os
FIRST_LINE = 6
parser = ArgumentParser()
parser.add_argument(
"--file",
"-f",
type=FileType("r"),
required=True,
help="Path to exported file",
)
parser.add_argument(
"--dest",
"-d",
type=Path,
required=True,
help="Destination directory",
)
parser.add_argument(
"--flatten",
action="store_true",
help="Create a flatten output"
)
args = parser.parse_args()
dest_path = args.dest.absolute()
os.makedirs(dest_path, exist_ok=True)
# Remove first few metadata lines
data = args.file.readlines()
data = "".join(data[FIRST_LINE:])
# For multi line tasks or notes
csv_stream = StringIO(data)
# process data and write
rows = DictReader(csv_stream)
for row in rows:
if row["Kind"] == "NOTE":
task_title = row["Title"]
task_list = row["List Name"]
task_content = row["Content"]
if args.flatten:
list_dir = dest_path
task_title = f"{task_list} - {task_title}"
else:
list_dir = os.path.join(dest_path, task_list)
os.makedirs(list_dir, exist_ok=True)
md_file_path = os.path.join(list_dir, f"{task_title}.md")
counter = 0
while os.path.exists(md_file_path):
md_file_path = os.path.join(list_dir, f"{task_title} - {counter:03}.md")
counter += 1
with open(md_file_path, "w") as md_file:
md_file.write(task_content)
@sgtcoder
Copy link

sgtcoder commented Sep 12, 2025

Here is an update to fix the nested folders along with the "Folder Name" also being used and a --hashtags option to append hashtags at end of file. Example importing into Obsidian

"""
Create markdown files based on your ticktick notes

You need to export your data from ticktick and use as input:
* Export from: Setting -> Account -> Generate Backup

Usage:
> python main.py -f ticktick.csv -d output_dir/ [--flatten] [--hashtags]

"""

from argparse import ArgumentParser, FileType
from csv import DictReader
from io import StringIO
from pathlib import Path
import os
import re

FIRST_LINE = 6

def sanitize_filename(filename):
    """Sanitize filename by removing/replacing invalid characters"""
    # Replace invalid characters with underscores
    filename = re.sub(r'[<>:"/\\|?*]', "_", filename)
    # Remove leading/trailing spaces and dots
    filename = filename.strip(" .")
    # Replace multiple spaces/underscores with single underscore
    filename = re.sub(r"[_\s]+", "_", filename)
    return filename

parser = ArgumentParser()
parser.add_argument(
    "--file",
    "-f",
    type=FileType("r"),
    required=True,
    help="Path to exported file",
)
parser.add_argument(
    "--dest",
    "-d",
    type=Path,
    required=True,
    help="Destination directory",
)
parser.add_argument("--flatten", action="store_true", help="Create a flatten output")
parser.add_argument("--hashtags", action="store_true", help="Add hashtags (#) to tags")
args = parser.parse_args()

dest_path = args.dest.absolute()
os.makedirs(dest_path, exist_ok=True)
# Remove first few metadata lines
data = args.file.readlines()
data = "".join(data[FIRST_LINE:])

# For multi line tasks or notes
csv_stream = StringIO(data)

# process data and write
rows = DictReader(csv_stream)
for row in rows:
    if row["Kind"] == "NOTE":
        task_title = row["Title"]
        task_list = row["List Name"]
        folder_name = row.get(
            "Folder Name", ""
        )  # Get folder name, empty string if not present
        task_content = row["Content"]

        # Sanitize folder and list names for folder creation
        sanitized_folder = sanitize_filename(folder_name) if folder_name else ""
        sanitized_list = sanitize_filename(task_list)

        if args.flatten:
            list_dir = dest_path
            if folder_name:
                task_title = f"{folder_name} - {task_list} - {task_title}"
            else:
                task_title = f"{task_list} - {task_title}"
        else:
            # Create hierarchical folder structure: Folder Name / List Name
            if folder_name:
                list_dir = dest_path / sanitized_folder / sanitized_list
            else:
                list_dir = dest_path / sanitized_list
            list_dir.mkdir(parents=True, exist_ok=True)

        # Sanitize the task title for filename
        sanitized_title = sanitize_filename(task_title)
        md_file_path = list_dir / f"{sanitized_title}.md"

        counter = 0
        while md_file_path.exists():
            md_file_path = list_dir / f"{sanitized_title} - {counter:03}.md"
            counter += 1

        with open(md_file_path, "w", encoding="utf-8") as md_file:
            # Append tags to content if Tags column has content
            tags = row.get("Tags", "").strip()
            if tags:
                if args.hashtags:
                    # Add hash tags in front of each tag
                    tag_list = [tag.strip() for tag in tags.split(",") if tag.strip()]
                    formatted_tags = " ".join([f"#{tag}" for tag in tag_list])
                else:
                    # Use tags as-is
                    formatted_tags = tags
                md_file.write(task_content)
                md_file.write(f"\n\n{formatted_tags}")
            else:
                md_file.write(task_content)

@aliva
Copy link
Author

aliva commented Sep 12, 2025

Here is an update to fix the nested folders along with the "Folder Name" also being used and a --hashtags option to append hashtags at end of file. Example importing into Obsidian

Thanks for sharing! :)

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