├── .gitignore
├── d1obsidian.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── thall.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcuserdata
│ └── thall.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── LICENSE
├── d1obsidian
├── Extensions.swift
└── main.swift
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/d1obsidian.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/d1obsidian.xcodeproj/project.xcworkspace/xcuserdata/thall.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/DayOne-Obsidian-Exporter/main/d1obsidian.xcodeproj/project.xcworkspace/xcuserdata/thall.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/d1obsidian.xcodeproj/xcuserdata/thall.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | d1obsidian.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/d1obsidian.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "81702b8f7fa6fc73458821cd4dfa7dde6684a9d1d4b6d63ecde0b6b832d8d75e",
3 | "pins" : [
4 | {
5 | "identity" : "yams",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/jpsim/Yams",
8 | "state" : {
9 | "revision" : "7568d1c6c63a094405afb32264c57dc4e1435835",
10 | "version" : "6.0.1"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Tyler Hall
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/d1obsidian/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // d1obsidian
4 | //
5 | // Created by Tyler Hall on 6/23/25.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Date {
11 | var year: String {
12 | let year = Calendar.current.component(.year, from: self)
13 | let nf = NumberFormatter()
14 | nf.minimumIntegerDigits = 2
15 | nf.maximumFractionDigits = 0
16 | return nf.string(from: NSNumber(integerLiteral: year))!
17 | }
18 |
19 | var month: String {
20 | let year = Calendar.current.component(.month, from: self)
21 | let nf = NumberFormatter()
22 | nf.minimumIntegerDigits = 2
23 | nf.maximumFractionDigits = 0
24 | return nf.string(from: NSNumber(integerLiteral: year))!
25 | }
26 |
27 | var day: String {
28 | let year = Calendar.current.component(.day, from: self)
29 | let nf = NumberFormatter()
30 | nf.minimumIntegerDigits = 2
31 | nf.maximumFractionDigits = 0
32 | return nf.string(from: NSNumber(integerLiteral: year))!
33 | }
34 |
35 | var key: String {
36 | let df = DateFormatter()
37 | df.dateFormat = "yyyy-MM-dd"
38 | return df.string(from: self)
39 | }
40 |
41 | var monthDirName: String {
42 | let df = DateFormatter()
43 | df.dateFormat = "yyyy-MM - MMMM"
44 | return df.string(from: self)
45 | }
46 | }
47 |
48 | extension String {
49 | func replace(regexPattern: String, replacement: String) -> String {
50 | guard let regex = try? NSRegularExpression(pattern: regexPattern, options: []) else {
51 | return self
52 | }
53 | let range = NSRange(self.startIndex..., in: self)
54 | return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replacement)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DayOne-Obsidian-Exporter
2 |
3 | I moved away from [Day One](https://dayoneapp.com/) and am now using [Obsidian](https://obsidian.md/) to keep my daily journal. To make that possible, I needed to migrate over a decade's worth of journal entries into an Obsidian-friendly format.
4 |
5 | This (very basic) Swift script takes [Day One's JSON export file](https://dayoneapp.com/guides/tips-and-tutorials/exporting-entries/) (and media attachments) and converts your entries into YAML + Markdown files that are friendly to Markdown editors like Obsidian. Entries are sorted into folders based on their date with support for multiple entries per day. Media attachments are saved alongside the Markdown files as inline attachments.
6 |
7 | Running the script
8 |
9 | ```
10 | d1obsidian
11 | ```
12 |
13 | will create a directory structure like this:
14 |
15 | ```
16 | /export-folder/
17 | /2023/
18 | /2024/
19 | /2025/
20 | /2024-01 - January/
21 | /2024-02 - Februrary/
22 | /2024-02-01/
23 | 2024-02-01 - 001.md
24 | 2024-02-01 - 002.md
25 | some-image.jpeg
26 | another-image.jpeg
27 | ```
28 |
29 | 
30 |
31 | Based on the availabe metadata, each entry's Markdown file will look similar to:
32 |
33 | ```
34 | ---
35 | uuid: AA7A6A77946547449ED0BBC99349537C
36 | creationDate: 2013-02-13T20:38:54Z
37 | timeZone: America/Chicago
38 | location:
39 | latitude: 37.546
40 | longitude: -77.439
41 | localityName: Richmond
42 | administrativeArea: Virginia
43 | country: United States
44 | weather:
45 | conditionsDescription: Cloudy
46 | weatherCode: cloudy
47 | temperatureCelsius: 3.5
48 | ---
49 | Today I went for a run and ate a sandwhich.
50 |
51 | 
52 | ```
53 |
54 | It's worth noting that this script does not exhaustively migrate all fields that Day One supports — mostly because I couldn't find a complete listing of all the available fields. It also strips out `dayone-moment:/workout` and `dayone-moment:/location` in-line references from your entires.
55 |
56 | And in keeping with open source tradition, there are plenty of other export scripts floating around GitHub and the Obsidian forums, but it was easier to hack this together quickly than try and modify someone else's to match the output I wanted.
57 |
--------------------------------------------------------------------------------
/d1obsidian/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // d1obsidian
4 | //
5 | // Created by Tyler Hall on 6/23/25.
6 | //
7 |
8 | import Foundation
9 | import Yams
10 |
11 | struct Journal: Codable {
12 | var entries: [Entry]
13 | }
14 |
15 | struct Entry: Codable {
16 | var uuid: String
17 | var text: String?
18 | var creationDate: Date
19 | var timeZone: String
20 | var location: Location?
21 | var weather: Weather?
22 | var photos: [Photo]?
23 | var videos: [Video]?
24 | }
25 |
26 | struct Location: Codable {
27 | var latitude: Double?
28 | var longitude: Double?
29 | var placeName: String?
30 | var localityName: String?
31 | var administrativeArea: String?
32 | var country: String?
33 | }
34 |
35 | struct Weather: Codable {
36 | var conditionsDescription: String
37 | var weatherCode: String
38 | var temperatureCelsius: Double
39 | }
40 |
41 | struct Photo: Codable {
42 | var orderInEntry: Int
43 | var identifier: String
44 | var type: String
45 | var md5: String
46 | }
47 |
48 | struct Video: Codable {
49 | var orderInEntry: Int
50 | var identifier: String
51 | var type: String
52 | var md5: String
53 | }
54 |
55 | let journalPath = CommandLine.arguments[1]
56 | let outputPath = CommandLine.arguments[2]
57 |
58 | let journalURL = URL(fileURLWithPath: journalPath)
59 | let outputDirURL = URL(fileURLWithPath: outputPath)
60 |
61 | let rootDirURL = journalURL.deletingLastPathComponent()
62 |
63 | let data = try! Data(contentsOf: journalURL)
64 |
65 | var allEntries: [String: [Entry]] = [:]
66 |
67 | do {
68 | let decoder = JSONDecoder()
69 | decoder.dateDecodingStrategy = .iso8601
70 |
71 | let journal = try decoder.decode(Journal.self, from: data)
72 | for var entry in journal.entries {
73 | if let text = entry.text {
74 | entry.text = text.replacingOccurrences(of: "\\", with: "")
75 | }
76 | let key = entry.creationDate.key
77 | if allEntries[key] == nil {
78 | allEntries[key] = []
79 | }
80 | allEntries[key]?.append(entry)
81 | }
82 | } catch {
83 | print(error)
84 | }
85 |
86 | let nfEntryCount = NumberFormatter()
87 | nfEntryCount.minimumIntegerDigits = 3
88 | nfEntryCount.maximumFractionDigits = 0
89 |
90 | for (key, entries) in allEntries {
91 | guard let firstEntryDate = entries.first?.creationDate as Date? else { continue }
92 | let entryDirURL = outputDirURL
93 | .appendingPathComponent(firstEntryDate.year)
94 | .appendingPathComponent(firstEntryDate.monthDirName)
95 | .appending(component: firstEntryDate.key)
96 | try? FileManager.default.createDirectory(at: entryDirURL, withIntermediateDirectories: true)
97 |
98 | let sortedEntries = entries.sorted { $0.creationDate < $1.creationDate }
99 |
100 | for i in 0..