Skip to content

Instantly share code, notes, and snippets.

@lzambarda
Last active January 5, 2023 15:24
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lzambarda/5e6cebd8356d3a2b5a2de01068745f5b to your computer and use it in GitHub Desktop.
Save lzambarda/5e6cebd8356d3a2b5a2de01068745f5b to your computer and use it in GitHub Desktop.
Google Keep to Standard Notes converter
/**
* Basic Google Keep to Standard Notes importer made by
* https://github.com/lzambarda because he was too lazy to manually migrate his
* notes.
*
* How to use:
* 1) Use Google Takeout to export a copy of your Keep notes. The folder you
* will download will contain two files for each note:
* - a sassy html visually resembling your note
* - a tasty JSON file containing the true format of the note
* 2) Edit KEEP_TAKEOUT_LOCATION to point to the location of the Keep folder
* 3) Explore some of the other flags (just a couple) in case you are fussy
* with tags and colors (for me it was good to convert colors to tags)
* 4) Run this script, it will produce a file in the same location
* 5) Go to your Standard Notes app > Account > Data Backups, toggle
* "Decrypted" and click on "Import Backup" (this should not overwrite your
* existing notes and tags but have a look at the "Unknowns" section below!)
* 6) ???
* 7) Profit!
*
* Important:
* 1) Read the comment of the constants since they can be used to manipulate the
* behaviour of the importer.
* 2) Checklist notes are converted to the following format:
* [ ] Note not ticked
* [x] Ticked note
* [x] Another ticked note
*
* Unknowns:
* 1) I haven't test what happens if you try to import a tag which already
* exists in your notes. I would make a test first if that was your case.
* 2) I am using a custom uuidv4 generator because I did not want to use
* packages. Although this script has a rudimentary collision check for
* generated uuid there could still be a collision when importing data. I
* don't know what that would cause!
* Usually this will cause duplication of notes due to different ids being
* produced.
*/
const fs = require("fs");
const path = require("path");
const HOME_DIR = require("os").homedir();
// https://stackoverflow.com/questions/105034/how-to-create-guid-uuid
// I know, this is not as safe as using the "uuid" npm package, but I was too
// lazy to set up a project with npm packages. This will suffice.
// By the way there is a rudimentary collision check below
const { randomBytes } = require("crypto");
const seenHashes = {};
function uuidv4() {
const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (randomBytes(4).readUInt32BE() * Math.pow(2, -32) * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
// Rudimentary collision checking
if (seenHashes.hasOwnProperty(uuid)) {
console.log(`uuid collision! ${uuid} has already been used`);
process.exit(1);
}
seenHashes[uuid] = true;
return uuid;
}
// Where to read Keep takeout data from.
const KEEP_TAKEOUT_LOCATION = path.join(HOME_DIR, "Desktop/Takeout/Keep");
// Standard Notes does not seem to have support for colored notes, but we can
// still convert them to tags.
const ADD_COLORS_AS_TAGS = true;
// By default Keep uses "DEFAULT" to identify notes with no color. Set this to
// true to avoid adding a tag for this "meta-color".
const IGNORE_DEFAULT_COLOR = false;
const now = new Date().toISOString();
const NEW_BACKUP_NAME = now + ".txt";
console.log("Reading", KEEP_TAKEOUT_LOCATION);
const backup = { items: [] };
const seenTags = {};
// We use this function with seenTags because each tag is only reported once as
// a single item containing the references to the notes using it.
function addTag(tagName, sourceNoteUUID) {
if (!seenTags.hasOwnProperty(tagName)) {
console.log(`New tag "${tagName}"`);
const tag = {
uuid: uuidv4(),
content_type: "Tag",
created_at: now,
updated_at: now,
content: {
title: tagName,
references: [],
appData: {
"org.standardnotes.sn": {
client_updated_at: now,
},
},
},
};
seenTags[tagName] = tag;
backup.items.push(tag);
}
seenTags[tagName].content.references.push({
uuid: sourceNoteUUID,
content_type: "Note",
});
}
const filenames = fs.readdirSync(KEEP_TAKEOUT_LOCATION);
filenames.forEach((filename) => {
if (!filename.endsWith(".json")) {
return;
}
console.log(`Processing "${filename}"`);
const content = fs.readFileSync(
path.join(KEEP_TAKEOUT_LOCATION, filename),
"utf-8"
);
src = JSON.parse(content);
t = new Date(src.userEditedTimestampUsec / 1000).toISOString();
dst = {
uuid: uuidv4(),
content_type: "Note",
created_at: t,
updated_at: t,
content: {
title: src.title,
references: [],
appData: {
"org.standardnotes.sn": {
client_updated_at: now,
archived: src.isArchived,
pinned: src.isPinned,
},
},
trashed: src.isTrashed,
},
};
// Compatibility with both checklists and text only
if (src.hasOwnProperty('textContent')) {
dst.content.text = src.textContent;
} else if (src.hasOwnProperty('listContent')) {
dst.content.text = src.listContent.map(i => `[${i.isChecked?'x':' '}] ${i.text}`).join('\n')
} else {
console.log(`WARNING: note ${src.title} seems to not have special content`)
}
if (
ADD_COLORS_AS_TAGS &&
(src.color !== "DEFAULT" || !IGNORE_DEFAULT_COLOR)
) {
addTag(src.color, dst.uuid);
}
if (src.hasOwnProperty("labels")) {
for (const label of src.labels) {
addTag(label.name, dst.uuid);
}
}
backup.items.push(dst);
});
fs.writeFileSync(NEW_BACKUP_NAME, JSON.stringify(backup));
console.log("Done");
@SamyDjemai
Copy link

SamyDjemai commented Jan 18, 2021

Thanks for the script! 👍
It initially only converted checklist notes, but it worked perfectly when I removed the return; on line 145

@lzambarda
Copy link
Author

Good catch, that was probably left over from testing!
Amended ✔️

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