├── .gitattributes ├── .gitignore ├── readme.md └── app.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.zip 3 | *.m4a 4 | *.jpg 5 | *.jpeg 6 | *.heic 7 | *.png 8 | out/ 9 | in/ -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Simple app that converts jsons(packed with media files into zip) exports from [Day One](https://dayoneapp.com/) to Markdown. 2 | 3 | I wrote it to export all my entries to [Obsidian](https://obsidian.md/). By default it uses Obsidian's ```![[]]``` linking, change relativeMediaLinking value inside to False if you want ```![]() ```. 4 | 5 | I haven't tested it for anything other than exporting text, headers, tags, date, photos and audios. Feel free to pr to make it less shitty. 6 | 7 | 8 | How to use: 9 | 1. Install python 3 10 | 2. Go to Dayone Export and click "Export Day One JSON". You can export everything to one zip file or have separate ones. 11 | 3. Download app.py 12 | 4. Create "in" folder in the same directory as app.py and place all zips you have there 13 | 5. Run ```python app.py``` 14 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import os 4 | import glob 5 | import zipfile 6 | import fnmatch 7 | import shutil 8 | import sys 9 | 10 | 11 | 12 | 13 | # If you use Obsidian.md you don't have to specifically point to media file as long as they are somewhere in "embedded media" folder. 14 | # If true link will be ![[file]] else ![](/folder/file) 15 | relativeMediaLinking = True 16 | 17 | 18 | # Cleans up text from "\special character" 19 | def cleanup(input): 20 | def quickreplace(a,b): 21 | nonlocal input 22 | input = input.replace(a,b) 23 | quickreplace("\.", ".") 24 | quickreplace("\)", ")") 25 | quickreplace("\(", "(") 26 | quickreplace(r"\\", r"\\"[:-1]) 27 | quickreplace("\+", "+") 28 | quickreplace("\!", "!") 29 | quickreplace("\-", "-") 30 | return input 31 | 32 | # Removes everything that can't be in file name 33 | def cleanFilename(input): 34 | def quickreplace(a,b): 35 | nonlocal input 36 | input = input.replace(a,b) 37 | 38 | quickreplace('"', "") 39 | quickreplace("*", "") 40 | quickreplace("\\", " ") 41 | quickreplace(r"/", " ") 42 | quickreplace("<", "") 43 | quickreplace(">", "") 44 | quickreplace(":", "") 45 | quickreplace("|", " ") 46 | quickreplace("?","") 47 | quickreplace(".", "") 48 | 49 | return input 50 | 51 | 52 | def processJson(readFrom, subfolder, tempsubfolder, outpath): 53 | 54 | with io.open(readFrom, encoding='utf-8') as read_file: 55 | data = json.load(read_file) 56 | 57 | # Setting and creating output folder structure 58 | folderpath = outpath + "/" + subfolder + "/" 59 | if not os.path.exists(folderpath): 60 | os.makedirs(folderpath) 61 | 62 | if not os.path.exists(folderpath + "/audios/"): 63 | os.makedirs(folderpath + "/audios/") 64 | if not os.path.exists(folderpath + "/photos/"): 65 | os.makedirs(folderpath + "/photos/") 66 | 67 | 68 | for entry in data['entries']: 69 | 70 | text = entry['text'] 71 | 72 | 73 | # 2020-01-31T09:44:02Z => 2020.01.31 09-44 74 | date = entry['creationDate'][:-4].replace("-", ".").replace(":", "-").replace("T", " ") 75 | 76 | 77 | # If first line starts with "# " — treat it as entry title 78 | if text.split("\n")[0][:2] == "# ": 79 | title = text.split("\n")[0].replace("# ", "").replace("#", "") 80 | else: 81 | title = "" 82 | 83 | 84 | # Process all "dayone-moments"(photos and audios) 85 | splitted = text.split("\n") 86 | filtered = fnmatch.filter(splitted, '![](dayone-moment:*)') 87 | 88 | for moment in filtered: 89 | # ![](dayone-moment:\/\/F7EEC3394BC0455FA513D9CAA0557C7E) => F7EEC3394BC0455FA513D9CAA0557C7E) = > F7EEC3394BC0455FA513D9CAA0557C7E 90 | momentIdentifier = moment.split("/")[-1] 91 | momentIdentifier = momentIdentifier[:-1] 92 | 93 | # Iterate over all media attached to entry and find filename and format of the right one 94 | if "audio" in moment: 95 | momentIn = entry['audios'] 96 | folder = 'audios' 97 | else: 98 | momentIn = entry['photos'] 99 | folder = 'photos' 100 | for momentItem in momentIn: 101 | if momentIdentifier == momentItem['identifier']: 102 | if "type" in momentItem: 103 | momentFormat = momentItem['type'] 104 | else: 105 | momentFormat = momentItem['format'] 106 | # For some reason "format" is codec not container — "aac" corresponds to "m4a" files. This is a stupid fix but still. 107 | if momentFormat == "aac": 108 | momentFormat = "m4a" 109 | 110 | # Jpegs have filenames. Audios don't 111 | if 'filename' in momentItem: 112 | # Filename has file extension in it, we only need the name. 113 | newName = momentItem['filename'].split('.')[0] 114 | else: 115 | if 'date' in momentItem: 116 | newName = momentItem['date'][:-4].replace("-", ".").replace(":", "-").replace("T", " ") + " " + momentIdentifier 117 | else: 118 | newName = date + " id " + momentIdentifier 119 | 120 | 121 | momentFile = momentItem['md5'] 122 | 123 | shutil.copy2(f'temp/{tempsubfolder}/{folder}/{momentFile}.{momentFormat}', f'./out/{subfolder}/{folder}/{newName}.{momentFormat}') 124 | 125 | if relativeMediaLinking: 126 | text = text.replace(moment, f"![[{newName}.{momentFormat}]]") 127 | else: 128 | text = text.replace(moment, f"![](/{folder}/{newName}.{momentFormat})") 129 | 130 | rawtags = entry.get('tags') 131 | 132 | writetags = False 133 | if rawtags: 134 | # We only need to append tags that aren't set in text 135 | filteredtags = [] 136 | for tag in rawtags: 137 | if "#"+ tag not in text: 138 | filteredtags.append(tag.replace(" ", "")) 139 | 140 | if len(filteredtags)>0: 141 | tagsString = "#" + " #".join(filteredtags) + "\n" 142 | writetags = True 143 | 144 | text = cleanup(text) 145 | title = cleanup(title) 146 | title = cleanFilename(title) 147 | 148 | newfilename = date +" — " + title + ".md" 149 | newfile = io.open(folderpath + "/" + newfilename , mode="a", encoding="utf-8") 150 | if writetags: 151 | newfile.write(tagsString) 152 | newfile.write(text) 153 | 154 | 155 | def ProcessZips(inpath, outpath): 156 | for zipname in os.listdir(inpath + "/"): 157 | if zipname.endswith(".zip"): 158 | zipnameClean = zipname.split(".")[0] 159 | with zipfile.ZipFile( inpath + "/" + zipname, 'r') as zip_ref: 160 | 161 | zip_ref.extractall("temp/" + zipnameClean) 162 | 163 | for jsonname in os.listdir("temp/" + zipnameClean): 164 | jsonnameClean = jsonname.split(".")[0] 165 | if jsonname.endswith(".json"): 166 | processJson("temp/" + zipnameClean + "/" + jsonname ,jsonnameClean, zipnameClean, outpath) 167 | shutil.rmtree('temp') 168 | 169 | 170 | 171 | 172 | if __name__ == "__main__": 173 | inpath = "in" 174 | outpath = "out" 175 | ProcessZips(inpath, outpath) 176 | print(f"Finished, check {outpath} folder") 177 | 178 | --------------------------------------------------------------------------------