├── .gitignore ├── README.md ├── index.js ├── markdown-create.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iNotes 2 | 3 | Free your notes on books from the clutches of Apple's iBooks. 4 | iNotes creates a markdown file, properly formatted, with the notes that you have made in the OSX version of iBooks. 5 | iBooks stores the notes in an sqlite DB. We sneak in and free the notes. 6 | 7 | Once imported into Ulysses, the fantastic writing app for OSX 8 | 9 | ![Ulyssess Screenshot](https://www.evernote.com/l/AA8HDCdqO6VHuoR8nvhmdcyUcoKr4Rt0CNoB/image.png) 10 | 11 | ## Setup and Using 12 | 13 | ``` 14 | npm install -g inotes 15 | inotes Downloads/ 16 | ``` 17 | 18 | ## Author 19 | 20 | [Shashank Mehta](https://shashankmehta.in) 21 | [@leostatic](http://twitter.com/leostatic) 22 | 23 | ## Licence 24 | MIT: [http://sm.mit-license.org](http://sm.mit-license.org) 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var sqlite3 = require('sqlite3').verbose(); 4 | var fs = require('fs'); 5 | var chalk = require('chalk'); 6 | var userHome = require('user-home'); 7 | var MD = require('./markdown-create.js'); 8 | 9 | const RESULT_DIRECTORY_NAME = "iBooks exports for Evernote"; 10 | const BOOKS_DIR = userHome + '/Library/Containers/com.apple.iBooksX/Data/Documents/BKLibrary'; 11 | const NOTES_DIR = userHome + '/Library/Containers/com.apple.iBooksX/Data/Documents/AEAnnotation' 12 | 13 | var args = process.argv.slice(2); 14 | var saveDir = args[0]; 15 | 16 | var colors = { 17 | ok: function(){ 18 | for(var i in arguments){ 19 | console.log(chalk.green(arguments[i])); 20 | } 21 | }, 22 | error: function(){ 23 | for(var i in arguments){ 24 | console.log(chalk.red(arguments[i])); 25 | } 26 | } 27 | } 28 | 29 | var system = { 30 | booksDB: null, 31 | notesDB: null, 32 | 33 | _checkDirExists: function(dir){ 34 | if(!fs.existsSync(dir)) { 35 | return false; 36 | } 37 | else return true; 38 | }, 39 | 40 | _getSQLiteFile: function(dir){ 41 | var files = fs.readdirSync(dir); 42 | for(var i in files){ 43 | if(files[i].substr(-6) === 'sqlite') 44 | return dir + '/' + files[i]; 45 | } 46 | return null; 47 | }, 48 | 49 | _verifyDB: function(){ 50 | if(this.booksDB === null){ 51 | colors.error("The Book Database doesn't seem to exist."); 52 | proc.exitWithError(); 53 | } 54 | 55 | if(this.notesDB === null){ 56 | colors.error("The notes database doesn't seem to exist."); 57 | proc.exitWithError(); 58 | } 59 | }, 60 | 61 | getDBNames: function(){ 62 | if(this._checkDirExists(BOOKS_DIR) === true){ 63 | system.booksDB = this._getSQLiteFile(BOOKS_DIR); 64 | } 65 | 66 | if(this._checkDirExists(NOTES_DIR) === true){ 67 | system.notesDB = this._getSQLiteFile(NOTES_DIR); 68 | } 69 | this._verifyDB(); 70 | } 71 | } 72 | 73 | var sql = { 74 | _dbBooks: null, 75 | _dbNotes: null, 76 | books: [], 77 | notes: [], 78 | _loaded: 0, 79 | 80 | init: function(){ 81 | this._dbBooks = new sqlite3.Database(system.booksDB); 82 | this._dbNotes = new sqlite3.Database(system.notesDB); 83 | }, 84 | 85 | getBooks: function(callback){ 86 | var self = this; 87 | var db = this._dbBooks; 88 | db.serialize(function(){ 89 | var query = "SELECT ZASSETID as id, ZTITLE AS title, ZAUTHOR AS author FROM ZBKLIBRARYASSET WHERE ZTITLE IS NOT NULL"; 90 | db.each(query, function(err, row) { 91 | if(err){ 92 | colors.error(err); 93 | proc.exitWithError(); 94 | } 95 | 96 | self.books[row['id']] = row; 97 | }, function(){ 98 | if(typeof callback === 'function'){ 99 | callback('books'); 100 | } 101 | }); 102 | }) 103 | }, 104 | 105 | getNotes: function(callback){ 106 | var self = this; 107 | var db = this._dbNotes; 108 | db.serialize(function(){ 109 | var query = "SELECT\ 110 | Z_PK as id,\ 111 | ZANNOTATIONREPRESENTATIVETEXT as fullText,\ 112 | ZANNOTATIONSELECTEDTEXT as note,\ 113 | ZFUTUREPROOFING5 as Chapter,\ 114 | ZANNOTATIONCREATIONDATE as Created,\ 115 | ZANNOTATIONMODIFICATIONDATE as Modified,\ 116 | ZANNOTATIONASSETID as bookID\ 117 | FROM ZAEANNOTATION\ 118 | WHERE ZANNOTATIONSELECTEDTEXT IS NOT NULL\ 119 | ORDER BY ZANNOTATIONASSETID ASC,Created ASC"; 120 | db.each(query, function(err, row) { 121 | if(err){ 122 | colors.error(err); 123 | proc.exitWithError(); 124 | } 125 | 126 | self.notes[row['id']] = row; 127 | }, function(){ 128 | if(typeof callback === 'function'){ 129 | callback('notes'); 130 | } 131 | }); 132 | 133 | }) 134 | }, 135 | 136 | collateNotes: function(){ 137 | var notes = this.notes; 138 | var books = this.books; 139 | for(var i in notes){ 140 | var note = notes[i]; 141 | if(typeof books[note.bookID] === 'undefined'){ 142 | continue; 143 | } 144 | 145 | if(typeof books[note.bookID].notes !== 'object'){ 146 | books[note.bookID].notes = []; 147 | } 148 | 149 | books[note.bookID].notes.push(note); 150 | } 151 | }, 152 | 153 | createMarkdown: function(){ 154 | var books = this.books; 155 | for(var i in this.books){ 156 | var book = books[i]; 157 | var md = new MD(book.title + ' _by ' + book.author + '_'); 158 | 159 | for(var i in book.notes){ 160 | var note = book.notes[i]; 161 | var fullText = this._textClean(note.fullText); 162 | var text = this._textClean(note.note); 163 | 164 | if(fullText !== text){ 165 | text = fullText.replace(text, '**' + text + '**'); 166 | } 167 | 168 | md.quote(text); 169 | } 170 | 171 | md.save(saveDir + book.title.replace(/ /g, '_') + '.md', function(err){ 172 | if(err){ 173 | colors.error(err); 174 | } 175 | else { 176 | colors.ok('Saved ' + book.title.replace(/ /g, '_')); 177 | } 178 | }); 179 | } 180 | }, 181 | 182 | _textClean: function(text){ 183 | text = text.replace(/\n/g, ' '); 184 | 185 | // Need to trim space first 186 | if(text.substr(-1) === ' '){ 187 | text = text.substr(0,text.length - 1); 188 | } 189 | if(text.substr(-1) === '.'){ 190 | text = text.substr(0,text.length - 1); 191 | } 192 | return text; 193 | }, 194 | 195 | close: function(){ 196 | this._dbBooks.close(); 197 | this._dbNotes.close(); 198 | }, 199 | 200 | loaded: function(type){ 201 | sql._loaded++; 202 | 203 | if(sql._loaded === 2){ 204 | sql.collateNotes(); 205 | sql.createMarkdown(); 206 | } 207 | } 208 | } 209 | 210 | var proc = { 211 | exit: function(){ 212 | process.exit(0); 213 | }, 214 | 215 | exitWithError: function(){ 216 | process.exit(1); 217 | } 218 | } 219 | 220 | var exec = function(){ 221 | if(typeof saveDir === 'undefined'){ 222 | colors.error('Please provide the download directory'); 223 | colors.error('\t eg: node index.js downloads'); 224 | proc.exitWithError(); 225 | } 226 | else if(saveDir.substr(-1) !== '/'){ 227 | saveDir+= '/'; 228 | } 229 | 230 | chalk.green('asd'); 231 | system.getDBNames(); 232 | sql.init(); 233 | sql.getBooks(sql.loaded); 234 | sql.getNotes(sql.loaded); 235 | } 236 | 237 | exec(); 238 | -------------------------------------------------------------------------------- /markdown-create.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var md = function(title){ 4 | this.data = ''; 5 | this.heading(title, 1); 6 | } 7 | 8 | md.prototype.append = function(text, newLine){ 9 | this.data += text; 10 | 11 | if(typeof newLine !== 'undefined'){ 12 | while(newLine > 0){ 13 | this.data+= '\n'; 14 | newLine--; 15 | } 16 | } 17 | } 18 | 19 | md.prototype.newLine = function(){ 20 | this.append('\n'); 21 | } 22 | 23 | md.prototype.doubleNewLine = function(){ 24 | this.append('\n\n'); 25 | } 26 | 27 | md.prototype.heading = function(text, level){ 28 | var suffix = ''; 29 | while(level > 0){ 30 | suffix+= '#'; 31 | level--; 32 | } 33 | suffix+= ' '; 34 | this.append(suffix + text, 2); 35 | } 36 | 37 | md.prototype.quote = function(text){ 38 | text.replace('\n', '\n> '); 39 | this.append('> ' + text, 2); 40 | } 41 | 42 | md.prototype.text = function(){ 43 | return this.data; 44 | } 45 | 46 | md.prototype.save = function(filename, callback){ 47 | if(fs.existsSync(filename)) { 48 | callback(new Error(filename + ' already exists')); 49 | return; 50 | } 51 | fs.writeFile(filename, this.data, function(err) { 52 | if(err) callback(err); 53 | 54 | callback(null); 55 | }); 56 | } 57 | 58 | module.exports = md; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inotes", 3 | "version": "1.0.1", 4 | "description": "Extract your notes from Apple's iBooks", 5 | "author": "Shashank Mehta ", 6 | "license": "MIT", 7 | "repository": "shashankmehta/inotes", 8 | "keywords": [ 9 | "notes", 10 | "highlights", 11 | "iBooks", 12 | "apple", 13 | "iNotes", 14 | "osx" 15 | ], 16 | "dependencies": { 17 | "chalk": "^1.0.0", 18 | "sqlite3": "^3.0.8", 19 | "user-home": "^1.1.1" 20 | }, 21 | "bin": { 22 | "inotes": "index.js" 23 | }, 24 | "preferGlobal": "true" 25 | } 26 | --------------------------------------------------------------------------------