Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A script to convert google keep notes to a bunch markdown files aiming mainly to nextcloud notes
#!/usr/bin/env python3
# from pdb import set_trace as bkp
from sys import argv, exit
from os import listdir, path, mkdir, utime
from json import loads as to_dict
from datetime import datetime as date
if '-h' in argv or '--help' in argv:
print('''
The commands will print those informations on notes:
-a all
-p pinned
-c note color
-l labels (it will print only when there's more than one)
-le last edit date
-s people who the note is shared with
None will be printed by default
''')
exit()
is_printing = {
'pinned': '-a' in argv or '-p' in argv,
'color': '-a' in argv or '-c' in argv,
'labels': '-a' in argv or '-l' in argv,
'last-edit': '-a' in argv or '-c' in argv,
'shared': '-a' in argv or '-c' in argv,
'none': len(argv) == 1
}
keep_dir = './Keep'
labels_file = 'Labels.txt'
nextcloud_dir = './nextcloud-notes'
if not path.exists(keep_dir):
print('"Keep" folder not found. Place this file on the same folder as "Keep" backup folder')
exit()
# get labels to create folders with the labels names
with open(path.join(keep_dir, labels_file), 'a+') as f:
f.seek(0)
labels = f.read().split('\n')
if len(labels): labels.pop()
labels.append('trash')
labels.append('archived')
# read files and separate the note jsons from attachments
files = listdir(keep_dir)
notes = list(filter(lambda f: '.json' in f, files))
attachments = list(filter(lambda f: '.json' not in f and '.html' not in f and f != labels_file, files))
print('{} notes found\n\n'.format(len(notes)))
def fix_filename(filename):
new_name = filename
for char in ['<', '>', ':', '"', '/', '\\', '|', '?', '*']:
new_name = new_name.replace(char, '-')
return new_name
# create nextcloud and label folders
if not path.exists(nextcloud_dir):
mkdir(nextcloud_dir)
print('created nextcloud folder')
for label in labels:
label_dir = path.join(nextcloud_dir, fix_filename(label))
if not path.exists(label_dir):
mkdir(label_dir)
print('created "{}" folder inside nextcloud folder'.format(label_dir))
# create notes
for note_file in notes:
# if note_file == 'file-to-debug.json': bkp()
print('processing note "{}"'.format(note_file))
note = to_dict(open(path.join(keep_dir, note_file)).read())
text = ''
note_labels = []
# simplify labels array
if 'labels' in note:
for l in note['labels']:
note_labels.append(l['name'])
# create note body
if note['title']: text += '# ' + note['title'] + '\n'
if is_printing['pinned'] and note['isPinned']: text += '**PINNED**\n\n'
elif note['title']: text += '\n'
#
# create note content
#
if 'textContent' in note: text += note['textContent']
elif 'listContent' in note:
# print MD list
for item in note['listContent']:
text += '- [{}] {}\n'.format('x' if item['isChecked'] else ' ', item['text'])
# attachments
if 'attachments' in note:
for a in note['attachments']:
text += '![]({})\n'.format(a['filePath'])
is_space_added = False
# add space before last information if it isn't added yet
def add_space():
global is_space_added
global text
if not is_space_added:
text += '\n'
is_space_added = True
# print color
if is_printing['color'] and ('color' in note and note['color'] != 'DEFAULT'):
add_space()
text += '\ncolor: ' + note['color']
# print label when it has more than one
if is_printing['labels'] and len(note_labels) > 1:
add_space()
text += '\nlabels: ' + str(note_labels)[1:len(str(note_labels))-1].replace('\'', '')
# print last edit time
if is_printing['last-edit']:
add_space()
timestamp = int(int(note['userEditedTimestampUsec']) / 1000 / 1000)
date_str = date.utcfromtimestamp(timestamp).strftime('%d %B %Y %H:%M:%S')
text += '\nlast edit: {}'.format(date_str)
# print if is shared
if is_printing['shared'] and 'sharees' in note:
add_space()
text += 'shared with: '
for u in note['sharees']:
text += '{} {}'.format(u['email'], 'is the owner\n' if u['isOwner'] else '')
#
# end note content creation
#
# decide path
path_name = ''
if len(note_labels): path_name = fix_filename(note_labels[0])
if note['isArchived']: path_name = 'archived'
elif note['isTrashed']: path_name = 'trash'
# create note
fname = '{}.{}'.format(note_file[0:len(note_file)-5], 'md')
f = open(path.join(nextcloud_dir, path_name, fname), 'w', encoding='utf-8')
# use the original modification time for the new file
original_mod_time = path.getmtime(path.join(keep_dir, note_file))
utime(path.join(nextcloud_dir, path_name, fname), (original_mod_time, original_mod_time))
f.write(text)
f.close()
print('note created inside "{}" folder\n'.format(path_name or 'nextcloud'))
print('\nAll notes converted and placed into {} folder'.format(nextcloud_dir))
@Mageek627
Copy link

Mageek627 commented Nov 26, 2020

This is very useful! Thanks a lot.

I would suggest a small improvement, in my case I didn't have any label, and so the file "Labels.txt" didn't exist, which resulted in "FileNotFoundError". I think line 41 should be replaced by:

with open(path.join(keep_dir, labels_file), 'a+') as f:
  f.seek(0)
  labels = f.read().split('\n')

Or alternatively, replace line 41 and 42 with:

try:
  labels = open(path.join(keep_dir, labels_file)).read().split('\n')
  labels.pop()
except:
  labels = []

I didn't thoroughly test it, but it seems to work fine like that.

@jhonasn
Copy link
Author

jhonasn commented Dec 7, 2020

Hello Mageek627 thanks for the suggestions, I'm very happy that this was useful for someone!

I will pick your first suggestion, I'm not using the script in the moment too so i will change it and commit for the next one who will use it, if there's a problem i hope the people notify me.

Thanks man!

@cidetraq
Copy link

cidetraq commented Mar 20, 2021

Just wanted to say thanks for providing this. Seems to have worked perfectly to convert, now I just need to import the resulting files into nextcloud.

@3ceepio
Copy link

3ceepio commented Jun 14, 2021

Many thanks for this, very useful. I hit errors to do with Unicode character mapping e.g.:

UnicodeEncodeError: 'charmap' codec can't encode character '\u200b' in position 1: character maps to <undefined>

So I changed line 149 from:

f = open(path.join(nextcloud_dir, path_name, fname), 'w')

to:

f = open(path.join(nextcloud_dir, path_name, fname), 'w', encoding="utf-8")

I know, hardcoding character for an environment is wrong but I figured this would be okay for a one-off conversion.

@jhonasn
Copy link
Author

jhonasn commented Jun 25, 2021

Just wanted to say thanks for providing this. Seems to have worked perfectly to convert, now I just need to import the resulting files into nextcloud.

I'm very glad that this had helped you! :)

@jhonasn
Copy link
Author

jhonasn commented Jun 25, 2021

Many thanks for this, very useful. I hit errors to do with Unicode character mapping e.g.:

UnicodeEncodeError: 'charmap' codec can't encode character '\u200b' in position 1: character maps to <undefined>

So I changed line 149 from:

f = open(path.join(nextcloud_dir, path_name, fname), 'w')

to:

f = open(path.join(nextcloud_dir, path_name, fname), 'w', encoding="utf-8")

I know, hardcoding character for an environment is wrong but I figured this would be okay for a one-off conversion.

thanks for the suggestion, i'll implement here, i'm very glad that this helped you!

@naudster
Copy link

naudster commented Oct 19, 2021

Thanks for this useful script.

This small change sets the file modification time so it matches the original Google file. I find this helpful as then the Notes app can sort by date.

Line 5:

from os import listdir, path, mkdir, utime

Insert at line 153:

# use the original modification time for the new file
original_mod_time = path.getmtime(path.join(keep_dir, note_file))
utime(path.join(nextcloud_dir, path_name, fname), (original_mod_time, original_mod_time))

@jhonasn
Copy link
Author

jhonasn commented Oct 19, 2021

Thanks for this useful script.

This small change sets the file modification time so it matches the original Google file. I find this helpful as then the Notes app can sort by date.

Line 5:

from os import listdir, path, mkdir, utime

Insert at line 153:

# use the original modification time for the new file original_mod_time = path.getmtime(path.join(keep_dir, note_file)) utime(path.join(nextcloud_dir, path_name, fname), (original_mod_time, original_mod_time))

You're welcome, thanks for the suggestion i'll make it right now

@dmonder
Copy link

dmonder commented Feb 14, 2022

The utime call needs to follow the f.close() statement. Otherwise, the close call will update the modification time to the current time. :)

@jhonasn
Copy link
Author

jhonasn commented Mar 26, 2022

The utime call needs to follow the f.close() statement. Otherwise, the close call will update the modification time to the current time. :)

man, there's a lot of time that I don't put my hands on this, can you send me an example for me to update the script?

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