Day One to Obsidian Conversion Script
A Python script that converts a directory of Day One JSON exports into Obsidian-compatible Markdown files. The distinguishing design choice versus similar tools (e.g. `dayone-to-obsidian`) is that it emits exactly one Markdown file per day, with all of that day’s Day One entries arranged chronologically inside it. That shape matches what the Obsidian daily-notes plugin ecosystem assumes, so the output drops into a daily-notes vault without further restructuring.
The implementation is deliberately simple — text templating rather than full JSON AST reconstruction — which makes it easy to adapt to a different note layout.
Known limitations
- Day One inserts extra spaces and adds/removes line breaks inconsistently. The script corrects the common cases (runs of blank lines, media embeds jammed against text, over-escaped punctuation) but not all of them — expect a hand-editing pass over the generated files.
- Fully fixing formatting would require parsing each entry’s internal JSON representation directly, a much larger script; the author judged manual review cheaper than writing that parser.
Usage
Run the script in a directory containing Day One JSON export files (*.json). It writes one YYYY-MM-DD DayName.md file per day into the same directory, each with frontmatter (title, date, Journal tag, aliases in several date formats) and each entry as a ## HH:MM | JournalName section with Dataview-style inline fields (dayOneId, datetime, tags, location, weather) above the entry text.
Media attachments (photos, videos, audio, PDFs) are rewritten from Day One dayone-moment: links into Obsidian !md5.ext“ embeds, so exported attachment files must be renamed to their md5 + extension for the embeds to resolve.
The script
#!/usr/bin/env python3
import os
import json
from datetime import datetime
from zoneinfo import ZoneInfo
# Read in all JSON files in the directory
#
rawDayOneData = {}
for file in os.listdir("."):
if file.endswith(".json"):
with open(file) as jsonData:
rawDayOneData[file.replace(".json", "")] = json.load(jsonData)
# Create master document list
#
journalData = {}
for journalName, rawJournalData in rawDayOneData.items():
for entry in rawJournalData["entries"]:
entryDateTimeObject = datetime.fromisoformat(entry["creationDate"])
entryDate = entryDateTimeObject.astimezone(ZoneInfo(entry["timeZone"])).strftime("%Y-%m-%d")
entryTime = entryDateTimeObject.astimezone(ZoneInfo(entry["timeZone"])).strftime("%H:%M")
entryDateTime = entryDateTimeObject.astimezone(ZoneInfo(entry["timeZone"])).strftime("%Y-%m-%dT%H:%M:%S")
entryUuid = entry["uuid"].lower()
entryIndex = entryDateTime + "|" + entryUuid
entryTags = "#" + journalName.replace(" ", "")
if "tags" in entry:
for tag in entry["tags"]:
entryTags = entryTags + " #" + tag.replace(" ", "")
if ("#Art" in entryTags) and ("#Photography" in entryTags):
entryHeader = "## " + entryTime + " | Photography"
else:
entryHeader = "## " + entryTime + " | " + journalName
entryLocation = ""
if "location" in entry:
entryLocation = "[" + entry["location"]["placeName"] + "](geo:" + str(round(entry["location"]["latitude"], 4)) + "," + str(round(entry["location"]["longitude"], 4)) + ")"
entryWeather = ""
if "weather" in entry:
entryWeather = str(int(round(entry["weather"]["temperatureCelsius"], 0))) + " °C"
if "conditionsDescription" in entry["weather"]:
entryWeather = entryWeather + " • " + entry["weather"]["conditionsDescription"]
elif "weatherCode" in entry["weather"]:
entryWeather = entryWeather + " • " + entry["weather"]["weatherCode"]
entryMedia = {}
if "audios" in entry:
for audioFile in entry["audios"]:
entryMedia["[dayone-moment:/audio/" + audioFile["identifier"] + "]"] = "![[" + audioFile["md5"] + audioFile["format"] + "]]"
if "pdfs" in entry:
for pdfFile in entry["pdfs"]:
entryMedia["[dayone-moment:/pdfAttachment/" + pdfFile["identifier"] + "]"] = "![[" + pdfFile["md5"] + ".pdf" + "]]"
if "photos" in entry:
for imageFile in entry["photos"]:
entryMedia["[dayone-moment://" + imageFile["identifier"] + "]"] = "![[" + imageFile["md5"] + "." + imageFile["type"] + "]]"
if "videos" in entry:
for videoFile in entry["videos"]:
entryMedia["[dayone-moment:/video/" + videoFile["identifier"] + "]"] = "![[" + videoFile["md5"] + "." + videoFile["type"] + "]]"
entryText = entry["text"]
for oldMediaLink, newMediaLink in entryMedia.items():
entryText = entryText.replace(oldMediaLink, newMediaLink)
entryText = entryText.replace("\n\n\n", "\n\n")
entryText = entryText.replace(" ![[", "\n\n![[")
entryText = entryText.replace("]]\n\n![[", "]]\n![[")
entryText = entryText.replace(".\n![[", ".\n\n![[")
entryText = entryText.replace("!\n![[", "!\n\n![[")
entryText = entryText.replace("?\n![[", "?\n\n![[")
entryText = entryText.replace(")\n![[", ")\n\n![[")
entryText = entryText.replace("\\&", "&")
entryText = entryText.replace("\\~", "~")
entryText = entryText.replace("\\-", "-")
entryText = entryText.replace("\\_", "_")
entryText = entryText.replace("\\+", "+")
entryText = entryText.replace("\\.", ".")
entryText = entryText.replace("\\!", "!")
entryText = entryText.replace("\\(", "(")
entryText = entryText.replace("\\)", ")")
entryText = entryText.replace("\\{", "{")
entryText = entryText.replace("\\}", "}")
entryText = entryText.replace("\\[\\]", "[]")
entryText = entryText.replace("\\\\", "\\")
entryText = entryText.replace("[ *", "[*")
entryText = entryText.replace("* ]", "*]")
entryText = entryText.replace(" --- ", " - ")
entryText = entryText.replace(" -- ", " - ")
entryText = entryText.strip()
if entryDate not in journalData:
journalData[entryDate] = {}
journalData[entryDate][entryIndex] = {}
journalData[entryDate][entryIndex]["header"] = entryHeader
journalData[entryDate][entryIndex]["uuid"] = entryUuid
journalData[entryDate][entryIndex]["datetime"] = entryDateTime
journalData[entryDate][entryIndex]["tags"] = entryTags
if len(entryLocation) > 0:
journalData[entryDate][entryIndex]["location"] = entryLocation
if len(entryWeather) > 0:
journalData[entryDate][entryIndex]["weather"] = entryWeather
journalData[entryDate][entryIndex]["text"] = entryText
journalData = dict(sorted(journalData.items()))
for journalDate, journalEntries in journalData.items():
journalData[journalDate] = dict(sorted(journalEntries.items()))
for journalDate, journalEntries in journalData.items():
journalDateTime = datetime.strptime(journalDate, "%Y-%m-%d")
journalDayNumber = journalDateTime.strftime("%-d")
journalDaySuffix = "th"
if (journalDayNumber == "1") or (journalDayNumber == "21") or (journalDayNumber == "31"):
journalDaySuffix = "st"
elif (journalDayNumber == "2") or (journalDayNumber == "22"):
journalDaySuffix = "nd"
elif (journalDayNumber == "3") or (journalDayNumber == "23"):
journalDaySuffix = "rd"
journalDayName = journalDateTime.strftime("%A")
journalHumanDate = journalDateTime.strftime("%B") + " " + journalDayNumber + journalDaySuffix + " " + journalDateTime.strftime("%Y")
journalAmericanDate = journalDateTime.strftime("%-m/%-d/%Y")
journalFileName = journalDate + " " + journalDayName + ".md"
with open (journalFileName, "w") as journalFile:
print("---", file = journalFile)
print("title: Journal for " + journalDayName + ", " + journalHumanDate, file = journalFile)
print("date: " + journalDate, file = journalFile)
print("tags:", file = journalFile)
print(" - Journal", file = journalFile)
print("aliases:", file = journalFile)
print(" - Journal for " + journalDayName + ", " + journalHumanDate, file = journalFile)
print(" - " + journalDayName + ", " + journalHumanDate, file = journalFile)
print(" - " + journalHumanDate, file = journalFile)
print(" - " + journalDate, file = journalFile)
print(" - " + journalAmericanDate, file = journalFile)
print("locations:", file = journalFile)
print("---", file = journalFile)
for entryId, entry in journalEntries.items():
print(entry["header"], file = journalFile)
print("[dayOneId:: " + entry["uuid"] + "]", file = journalFile)
print("[datetime:: " + entry["datetime"] + "]", file = journalFile)
print("[tags:: " + entry["tags"] + "]", file = journalFile)
if "location" in entry:
print("[location:: " + entry["location"] + "]", file = journalFile)
if "weather" in entry:
print("[weather:: " + entry["weather"] + "]", file = journalFile)
print("", file = journalFile)
print(entry["text"], file = journalFile)
print("", file = journalFile)Note: the media-link replacement keys above reproduce the original
dayone-moment:URL forms; Day One’s exact link syntax has varied across export versions, so verify one converted file against its source entry before running a full archive through.
Related
- spaced-repetition — another page in this wiki about personal note-taking practice
- the wiki
index.md, itself maintained as Markdown under version control like the output of this script