├── .gitignore ├── index.js ├── package-lock.json ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .onsave 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pify = require(`pify`) 4 | const { readFile, writeFile, utimes } = pify(require(`fs`)) 5 | const { join: joinPath } = require(`path`) 6 | const { get } = require(`httpie`) 7 | const makeDir = require(`make-dir`) 8 | const filenamify = require(`filenamify`) 9 | const untildify = require(`untildify`) 10 | 11 | const catchify = promise => promise.then(result => [ null, result ]).catch(err => [ err, null ]) 12 | 13 | // https://www.diigo.com/api_dev 14 | const buildUrl = ({ count, step, apiKey, user }) => `https://secure.diigo.com/api/v2/bookmarks?key=${ apiKey }&user=${ user }&start=${ step * count }&count=${ count }&sort=1&filter=all` 15 | 16 | const buildHeaders = ({ user, password }) => ({ 17 | Authorization: `Basic ${ Buffer.from(`${ user }:${ password }`).toString(`base64`) }`, 18 | }) 19 | 20 | const diigoAndUserContentSeparator = `---\n` 21 | const extension = `.md` 22 | 23 | const stepper = async(increment, fn) => { 24 | let current = 0 25 | let shouldContinue = true 26 | 27 | while (shouldContinue) { 28 | shouldContinue = await fn(current) 29 | ++current 30 | } 31 | } 32 | 33 | const main = async({ user, password, path: potentiallyTildifiedPath, all, apiKey, countPerRequest, datePrefix }) => { 34 | const headers = buildHeaders({ user, password }) 35 | const count = parseInt(countPerRequest, 10) 36 | 37 | const path = untildify(potentiallyTildifiedPath) 38 | 39 | await makeDir(path) 40 | 41 | let synced = 0 42 | 43 | await stepper(countPerRequest, async step => { 44 | const { data: bookmarks } = await get(buildUrl({ count, step, apiKey, user }), { headers }) 45 | 46 | await updateBookmarksOnDisc({ bookmarks, path, datePrefix }) 47 | 48 | synced += bookmarks.length 49 | 50 | return all && bookmarks.length === count 51 | }) 52 | 53 | console.log(`Synced`, synced, `bookmarks.`) 54 | } 55 | 56 | const updateBookmarksOnDisc = async({ bookmarks, path, datePrefix }) => { 57 | const now = new Date() 58 | 59 | return Promise.all(bookmarks.map( 60 | async({ tags, url, title, created_at: createdAt, updated_at: updatedAt, desc: description }) => { 61 | const sanitizedName = filenamify(title, { 62 | replacement: `_`, 63 | maxLength: 255 - extension.length, 64 | }) 65 | const fullPath = joinPath(path, sanitizedName + extension) 66 | const day = new Date(createdAt).toISOString().slice(0, 10) 67 | const diigoData = noteContents({ tags, url, title, day, datePrefix }) 68 | 69 | const [ err, currentContents ] = await catchify(readFile(fullPath, { encoding: `utf8` })) 70 | 71 | const setModifiedTime = () => utimes(fullPath, now, new Date(updatedAt)) 72 | 73 | if (err && err.code === `ENOENT`) { 74 | const descriptionWithOptionalWhitespace = description ? (description + `\n`) : `` 75 | await writeFile(fullPath, diigoData + diigoAndUserContentSeparator + `\n` + descriptionWithOptionalWhitespace) 76 | await setModifiedTime() 77 | } else if (err) { 78 | throw err 79 | } else { 80 | const [ , ...rest ] = currentContents.split(diigoAndUserContentSeparator) 81 | await writeFile(fullPath, diigoData + diigoAndUserContentSeparator + rest.join(diigoAndUserContentSeparator)) 82 | await setModifiedTime() 83 | } 84 | }, 85 | )) 86 | } 87 | 88 | const noteContents = ({ tags, url, title, day, datePrefix }) => 89 | `# ${ title } 90 | 91 | - tags: ${ tags.split(/,\s*/g).map(tag => `#${ tag.replace(/_/g, `-`) }`).join(` `) } 92 | - url: ${ url } 93 | - cached: [On Diigo](https://www.diigo.com/cached?url=${ encodeURIComponent(url) }) 94 | - created: [[${ datePrefix }${ day }|${ day }]] 95 | 96 | ` 97 | 98 | const args = require(`mri`)(process.argv.slice(2), { 99 | string: [ `user`, `password`, `path`, `apiKey`, `countPerRequest`, `datePrefix` ], 100 | boolean: `all`, 101 | default: { 102 | all: false, 103 | countPerRequest: 20, 104 | datePrefix: ``, 105 | }, 106 | }) 107 | 108 | main(args).catch(err => { 109 | console.error(err) 110 | process.exit(1) 111 | }) 112 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sync-diigo-to-folder", 3 | "version": "2.4.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "escape-string-regexp": { 8 | "version": "1.0.5", 9 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 10 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 11 | }, 12 | "filename-reserved-regex": { 13 | "version": "2.0.0", 14 | "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", 15 | "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=" 16 | }, 17 | "filenamify": { 18 | "version": "4.1.0", 19 | "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.1.0.tgz", 20 | "integrity": "sha512-KQV/uJDI9VQgN7sHH1Zbk6+42cD6mnQ2HONzkXUfPJ+K2FC8GZ1dpewbbHw0Sz8Tf5k3EVdHVayM4DoAwWlmtg==", 21 | "requires": { 22 | "filename-reserved-regex": "^2.0.0", 23 | "strip-outer": "^1.0.1", 24 | "trim-repeated": "^1.0.0" 25 | } 26 | }, 27 | "httpie": { 28 | "version": "1.1.2", 29 | "resolved": "https://registry.npmjs.org/httpie/-/httpie-1.1.2.tgz", 30 | "integrity": "sha512-VQ82oXG95oY1fQw/XecHuvcFBA+lZQ9Vwj1RfLcO8a7HpDd4cc2ukwpJt+TUlFaLUAzZErylxWu6wclJ1rUhUQ==" 31 | }, 32 | "make-dir": { 33 | "version": "3.1.0", 34 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", 35 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 36 | "requires": { 37 | "semver": "^6.0.0" 38 | } 39 | }, 40 | "mri": { 41 | "version": "1.1.6", 42 | "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz", 43 | "integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==" 44 | }, 45 | "pify": { 46 | "version": "5.0.0", 47 | "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", 48 | "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" 49 | }, 50 | "semver": { 51 | "version": "6.3.0", 52 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 53 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 54 | }, 55 | "strip-outer": { 56 | "version": "1.0.1", 57 | "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", 58 | "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", 59 | "requires": { 60 | "escape-string-regexp": "^1.0.2" 61 | } 62 | }, 63 | "trim-repeated": { 64 | "version": "1.0.0", 65 | "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", 66 | "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", 67 | "requires": { 68 | "escape-string-regexp": "^1.0.2" 69 | } 70 | }, 71 | "untildify": { 72 | "version": "4.0.0", 73 | "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", 74 | "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sync-diigo-to-folder", 3 | "version": "2.4.0", 4 | "description": "Sync all your Diigo bookmarks to a directory as Markdown files. Intended for use with Obsidian", 5 | "main": "index.js", 6 | "bin": "./index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified :-|\" && exit 0" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/TehShrike/sync-diigo-to-folder.git" 13 | }, 14 | "keywords": [ 15 | "diigo" 16 | ], 17 | "author": "TehShrike", 18 | "license": "WTFPL", 19 | "bugs": { 20 | "url": "https://github.com/TehShrike/sync-diigo-to-folder/issues" 21 | }, 22 | "homepage": "https://github.com/TehShrike/sync-diigo-to-folder#readme", 23 | "dependencies": { 24 | "filenamify": "^4.1.0", 25 | "httpie": "^1.1.2", 26 | "make-dir": "^3.1.0", 27 | "mri": "^1.1.6", 28 | "pify": "^5.0.0", 29 | "untildify": "^4.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Sync all your Diigo bookmarks to a directory as Markdown files. Intended for use with [Obsidian](https://obsidian.md/). 2 | 3 | ## Install 4 | 5 | ```sh 6 | npm i -g sync-diigo-to-folder 7 | ``` 8 | 9 | ## Use 10 | 11 | ```sh 12 | sync-diigo-to-folder --path=/Users/tehshrike/Obsidian/Bookmarks --all --user=DIIGO_USERNAME --password=DIIGO_PASSWORD --apiKey=DIIGO_API_KEY 13 | ``` 14 | 15 | This script is meant to be idempotent, so that you can re-run it over and over without losing any data other than what originally came from Diigo. 16 | 17 | By default it only reads the most recently-updated batch of bookmarks. 18 | 19 | ### Arguments 20 | 21 | - `path`: the directory to write output files to 22 | - `all`: *(default off)* – whether to save the most recently-updated bookmarks, or only one request's worth 23 | - `countPerRequest`: *(default 20)* – How many bookmarks to fetch per API request. Max 100. 24 | - `user`: your Diigo username 25 | - `password`: your Diigo password 26 | - `apiKey`: your [Diigo API key](https://www.diigo.com/api_keys/new/) 27 | - `datePrefix`: a string to prefix the `[[YYYY-MM-DD]]` date links with (e.g. `Day/`) 28 | 29 | ## Output 30 | 31 | Right now the output for a bookmark of a site like looks like: 32 | 33 | ```md 34 | # How (some) good corporate engineering blogs are written 35 | 36 | - tags: #writing #marketing #blogging 37 | - url: https://danluu.com/corp-eng-blogs/ 38 | - cached: [On Diigo](https://www.diigo.com/cached?url=https%3A%2F%2Fdanluu.com%2Fcorp-eng-blogs%2F) 39 | - created: [[2020-07-13]] 40 | 41 | --- 42 | 43 | 44 | ``` 45 | 46 | the intention is that you can put your own notes below the `---` separator as desired. Any changes above the separator will be overwritten by changes to your bookmark in Diigo. 47 | 48 | If you typed a description into Diigo, that description will be placed below the `---` separator on first write. Updated descriptions will not be written to a pre-existing file. 49 | 50 | ## License 51 | 52 | [WTFPL](https://wtfpl2.com) 53 | --------------------------------------------------------------------------------