Last active
January 5, 2023 15:24
-
-
Save lzambarda/5e6cebd8356d3a2b5a2de01068745f5b to your computer and use it in GitHub Desktop.
Google Keep to Standard Notes converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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"); |
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
Thanks for the script! 👍
It initially only converted checklist notes, but it worked perfectly when I removed the
return;
on line 145