Skip to content

Instantly share code, notes, and snippets.

@jhonasn
Last active July 16, 2026 18:33
Show Gist options
  • Select an option

  • Save jhonasn/479f28360041834a064163352d06e9fc to your computer and use it in GitHub Desktop.

Select an option

Save jhonasn/479f28360041834a064163352d06e9fc to your computer and use it in GitHub Desktop.
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 '-le' 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), encoding='utf-8').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')
with open(path.join(nextcloud_dir, path_name, fname), 'w', encoding='utf-8') as f:
# 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)
print('note created inside "{}" folder\n'.format(path_name or 'nextcloud'))
print('\nAll notes converted and placed into {} folder'.format(nextcloud_dir))
@dmonder

dmonder commented Feb 14, 2022

Copy link
Copy Markdown

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

@jhonasn

jhonasn commented Mar 26, 2022

Copy link
Copy Markdown
Author

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?

@Expello

Expello commented Jun 24, 2024

Copy link
Copy Markdown

does anyone have an idea how to extend this ingenious script so that the german umlauts (ä, ö, ü, ß) are converted correctly?

EDIT: ok it was easier than I thought

Line 77:

From: note = to_dict(open(path.join(keep_dir, note_file)).read())
to this: note = to_dict(open(path.join(keep_dir, note_file), encoding='utf-8').read())

@jhonasn

jhonasn commented Nov 26, 2024

Copy link
Copy Markdown
Author

updated line 77 as @Expello mentioned

@Jesus-M

Jesus-M commented May 30, 2026

Copy link
Copy Markdown

Hello,

thanks for the script, it saved my day :-)

I think there is a small mistake, line 27 'last-edit': '-a' in argv or '-c' in argv, should be 'last-edit': '-a' in argv or '-le' in argv,

And related to this, to keep even better control of the imported notes, could the updated timestamp of the new files be changed to that 'last edit' date?

I did it with a small script after importing the notes, but I think it'd be natural to do it during the import time.

Thanks!

@jhonasn

jhonasn commented Jul 16, 2026

Copy link
Copy Markdown
Author

Hello,

thanks for the script, it saved my day :-)

I think there is a small mistake, line 27 'last-edit': '-a' in argv or '-c' in argv, should be 'last-edit': '-a' in argv or '-le' in argv,

And related to this, to keep even better control of the imported notes, could the updated timestamp of the new files be changed to that 'last edit' date?

I did it with a small script after importing the notes, but I think it'd be natural to do it during the import time.

Thanks!

hey, I'm glad that this old script still could help you!

thanks for pointing the typo on -le, I fixed it now.

about the updated timestamp , man, I don't use this script since I created it many years ago, if you implemented the please share with me so that I can update this script for the next person that use it.

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