├── .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 | ![SS-20250623 130851 Journal@2x](https://github.com/user-attachments/assets/88023a33-3d05-4b0a-9b7b-7a1b557f71e9) 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 | ![](991F17490A0F4A919116C5AB428E6F1E.jpeg) 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..