├── .env.example ├── .gitignore ├── TODO.md ├── example-raindrop-data.json ├── package.json ├── README.md ├── CHANGELOG.md ├── example-github-data.json └── index.js /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN=YOUR-TOKEN-HERE 2 | RAINDROP_TOKEN=YOUR-TOKEN-HERE 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | .DS_Store 4 | data.json 5 | newData.json 6 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - [ ] Add a Promise.all method to the multiple post request done via the single request loop. 4 | -------------------------------------------------------------------------------- /example-raindrop-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionId": 18455651, 3 | "title": "kamens/jQuery-menu-aim", 4 | "link": "https://github.com/kamens/jQuery-menu-aim", 5 | "tags": [ 6 | "github", 7 | "JavaScript" 8 | ], 9 | "created": "2013-03-03T02:50:35Z", 10 | "pleaseParse": {} 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-starred-repos-to-raindrop-io", 3 | "version": "1.0.0", 4 | "description": "Add all of your starred repos to raindrop.io", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "dotenv": "^10.0.0", 15 | "isomorphic-fetch": "^3.0.0", 16 | "node-fetch": "^2.6.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Add your github starred repos to raindrop.io as bookmarks 2 | 3 | This script will automatically add all of your starred github repos (the ones you starred from other people) into raindrop.io as bookmarks. 4 | 5 | ## What you need to do? 6 | 7 | 1. Copy and rename the `.env.example` to `.env`; 8 | 2. Generate a personal token for github and add it to the `.env` file ; 9 | 3. Generate an authentication token in the raindrop.io app: 10 | 1. Go to -> ; 11 | 2. Then click `+ Create a new app` under the `For Developers` section; 12 | 3. Now, click on the name of your fresh generated app and then click in `Create test token`; 13 | 4. Finally copy this `test token` in the `.env` file; 14 | 4. Follow the steps described in the `index.js` starting at line `166`; 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### To Add 11 | 12 | ### To Change 13 | 14 | ### To Remove 15 | 16 | ### To Fix 17 | 18 | ## [1.0.0] - 2021-06-06 19 | 20 | ### Added 21 | 22 | - Fetch all starred repositories from Github 23 | - Parse starred repositories to be in the expected format for raindrop.io 24 | - Add methods for single post, multiple post by 100's and multiple post by single loops with 1+ minutes delay in between (API restriction) 25 | - Create documentation and TODO list 26 | 27 | [unreleased]: https://github.com/azedo/raindrop-io-github-starred-repos/compare/v1.0.0...main 28 | [0.0.1]: https://github.com/azedo/raindrop-io-github-starred-repos/releases/tag/v1.0.0 29 | -------------------------------------------------------------------------------- /example-github-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2126244, 3 | "node_id": "MDEwOlJlcG9zaXRvcnkyMTI2MjQ0", 4 | "name": "bootstrap", 5 | "full_name": "twbs/bootstrap", 6 | "private": false, 7 | "owner": { 8 | "login": "twbs", 9 | "id": 2918581, 10 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjI5MTg1ODE=", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/2918581?v=4", 12 | "gravatar_id": "", 13 | "url": "https://api.github.com/users/twbs", 14 | "html_url": "https://github.com/twbs", 15 | "followers_url": "https://api.github.com/users/twbs/followers", 16 | "following_url": "https://api.github.com/users/twbs/following{/other_user}", 17 | "gists_url": "https://api.github.com/users/twbs/gists{/gist_id}", 18 | "starred_url": "https://api.github.com/users/twbs/starred{/owner}{/repo}", 19 | "subscriptions_url": "https://api.github.com/users/twbs/subscriptions", 20 | "organizations_url": "https://api.github.com/users/twbs/orgs", 21 | "repos_url": "https://api.github.com/users/twbs/repos", 22 | "events_url": "https://api.github.com/users/twbs/events{/privacy}", 23 | "received_events_url": "https://api.github.com/users/twbs/received_events", 24 | "type": "Organization", 25 | "site_admin": false 26 | }, 27 | "html_url": "https://github.com/twbs/bootstrap", 28 | "description": "The most popular HTML, CSS, and JavaScript framework for developing responsive, mobile first projects on the web.", 29 | "fork": false, 30 | "url": "https://api.github.com/repos/twbs/bootstrap", 31 | "forks_url": "https://api.github.com/repos/twbs/bootstrap/forks", 32 | "keys_url": "https://api.github.com/repos/twbs/bootstrap/keys{/key_id}", 33 | "collaborators_url": "https://api.github.com/repos/twbs/bootstrap/collaborators{/collaborator}", 34 | "teams_url": "https://api.github.com/repos/twbs/bootstrap/teams", 35 | "hooks_url": "https://api.github.com/repos/twbs/bootstrap/hooks", 36 | "issue_events_url": "https://api.github.com/repos/twbs/bootstrap/issues/events{/number}", 37 | "events_url": "https://api.github.com/repos/twbs/bootstrap/events", 38 | "assignees_url": "https://api.github.com/repos/twbs/bootstrap/assignees{/user}", 39 | "branches_url": "https://api.github.com/repos/twbs/bootstrap/branches{/branch}", 40 | "tags_url": "https://api.github.com/repos/twbs/bootstrap/tags", 41 | "blobs_url": "https://api.github.com/repos/twbs/bootstrap/git/blobs{/sha}", 42 | "git_tags_url": "https://api.github.com/repos/twbs/bootstrap/git/tags{/sha}", 43 | "git_refs_url": "https://api.github.com/repos/twbs/bootstrap/git/refs{/sha}", 44 | "trees_url": "https://api.github.com/repos/twbs/bootstrap/git/trees{/sha}", 45 | "statuses_url": "https://api.github.com/repos/twbs/bootstrap/statuses/{sha}", 46 | "languages_url": "https://api.github.com/repos/twbs/bootstrap/languages", 47 | "stargazers_url": "https://api.github.com/repos/twbs/bootstrap/stargazers", 48 | "contributors_url": "https://api.github.com/repos/twbs/bootstrap/contributors", 49 | "subscribers_url": "https://api.github.com/repos/twbs/bootstrap/subscribers", 50 | "subscription_url": "https://api.github.com/repos/twbs/bootstrap/subscription", 51 | "commits_url": "https://api.github.com/repos/twbs/bootstrap/commits{/sha}", 52 | "git_commits_url": "https://api.github.com/repos/twbs/bootstrap/git/commits{/sha}", 53 | "comments_url": "https://api.github.com/repos/twbs/bootstrap/comments{/number}", 54 | "issue_comment_url": "https://api.github.com/repos/twbs/bootstrap/issues/comments{/number}", 55 | "contents_url": "https://api.github.com/repos/twbs/bootstrap/contents/{+path}", 56 | "compare_url": "https://api.github.com/repos/twbs/bootstrap/compare/{base}...{head}", 57 | "merges_url": "https://api.github.com/repos/twbs/bootstrap/merges", 58 | "archive_url": "https://api.github.com/repos/twbs/bootstrap/{archive_format}{/ref}", 59 | "downloads_url": "https://api.github.com/repos/twbs/bootstrap/downloads", 60 | "issues_url": "https://api.github.com/repos/twbs/bootstrap/issues{/number}", 61 | "pulls_url": "https://api.github.com/repos/twbs/bootstrap/pulls{/number}", 62 | "milestones_url": "https://api.github.com/repos/twbs/bootstrap/milestones{/number}", 63 | "notifications_url": "https://api.github.com/repos/twbs/bootstrap/notifications{?since,all,participating}", 64 | "labels_url": "https://api.github.com/repos/twbs/bootstrap/labels{/name}", 65 | "releases_url": "https://api.github.com/repos/twbs/bootstrap/releases{/id}", 66 | "deployments_url": "https://api.github.com/repos/twbs/bootstrap/deployments", 67 | "created_at": "2011-07-29T21:19:00Z", 68 | "updated_at": "2021-06-05T14:24:41Z", 69 | "pushed_at": "2021-06-05T09:20:25Z", 70 | "git_url": "git://github.com/twbs/bootstrap.git", 71 | "ssh_url": "git@github.com:twbs/bootstrap.git", 72 | "clone_url": "https://github.com/twbs/bootstrap.git", 73 | "svn_url": "https://github.com/twbs/bootstrap", 74 | "homepage": "https://getbootstrap.com", 75 | "size": 183847, 76 | "stargazers_count": 150669, 77 | "watchers_count": 150669, 78 | "language": "JavaScript", 79 | "has_issues": true, 80 | "has_projects": true, 81 | "has_downloads": true, 82 | "has_wiki": false, 83 | "has_pages": true, 84 | "forks_count": 73556, 85 | "mirror_url": null, 86 | "archived": false, 87 | "disabled": false, 88 | "open_issues_count": 377, 89 | "license": { 90 | "key": "mit", 91 | "name": "MIT License", 92 | "spdx_id": "MIT", 93 | "url": "https://api.github.com/licenses/mit", 94 | "node_id": "MDc6TGljZW5zZTEz" 95 | }, 96 | "forks": 73556, 97 | "open_issues": 377, 98 | "watchers": 150669, 99 | "default_branch": "main", 100 | "permissions": { 101 | "admin": false, 102 | "push": false, 103 | "pull": true 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import fetch from "isomorphic-fetch"; 3 | import { writeFileSync, readFileSync } from 'fs'; 4 | 5 | dotenv.config(); 6 | 7 | // Temporary variable to store github pages 8 | let file 9 | 10 | /** 11 | * First function - Get starred repos list 12 | * 13 | * @param {string} url 14 | * @param {number} [page=1] 15 | * @param {any | null} [previousContent=null] 16 | */ 17 | async function getGithubStarsList(url, page = 1, previousContent = null) { 18 | console.log(`Fetching page ${page}...`) 19 | 20 | const fetchUrl = `${url}?page=${page}` 21 | const content = await fetch(fetchUrl, { 22 | headers: new Headers({ 23 | authorization: `token ${process.env.GITHUB_TOKEN}` 24 | }) 25 | }).then(res => res.json()); 26 | const contentLength = await content.length 27 | const shouldFetchAgain = contentLength > 0 28 | 29 | if (previousContent) { 30 | file = previousContent.concat(content) 31 | } else { 32 | file = content 33 | } 34 | 35 | if (shouldFetchAgain) { 36 | const nextPage = page + 1; 37 | await getGithubStarsList('https://api.github.com/users/azedo/starred', nextPage, file); 38 | } else { 39 | console.log(`Page ${page - 1} was the last valid page!`) 40 | console.log('Saving file...') 41 | await writeFileSync('./data.json', JSON.stringify(file)) 42 | console.log('Done!') 43 | } 44 | } 45 | 46 | /** 47 | * Second function - Parse the github list file and add the necessary fields 48 | */ 49 | async function readAndParseGithubData() { 50 | const dataFile = readFileSync('./data.json', { encoding: 'utf-8' }) 51 | const parsed = JSON.parse(dataFile) 52 | 53 | let total = parsed.length 54 | let count = parsed.length - 100 55 | let res = [] 56 | 57 | while (total > 0) { 58 | const arrayContent = parsed.slice(count, total) 59 | 60 | if (arrayContent.length > 0) { 61 | res = res.concat([arrayContent]) 62 | } 63 | 64 | total = total - 100 65 | count = count - 100 66 | } 67 | 68 | if (total > -100) { 69 | total = total + 100 70 | res = res.concat([parsed.slice(0, total)]) 71 | } 72 | 73 | const resParsed = await res.map((item, i) => { 74 | console.log(`${i} = ${item.length}`) 75 | 76 | return item.map(it => ({ 77 | collectionId: 18455651, 78 | title: it.full_name, 79 | link: it.html_url, 80 | tags: ["github", `${it.language}`], 81 | created: it.created_at, 82 | excerpt: it.description 83 | })) 84 | }) 85 | console.log(`total = ${resParsed.length}`) 86 | await writeFileSync('./newData.json', JSON.stringify(resParsed)) 87 | } 88 | 89 | /** 90 | * Create a single entry in the app 91 | * 92 | * @param {collectionId: number, title: string, link: string, tags: [string], created: any, description: string} item The item object 93 | */ 94 | async function createNewRainDrop(item) { 95 | const fetchUrl = 'https://api.raindrop.io/rest/v1/raindrop' 96 | 97 | let response = await fetch(fetchUrl, { 98 | method: "POST", 99 | headers: new Headers({ 100 | "Authorization": `Bearer ${process.env.RAINDROP_TOKEN}`, 101 | "Content-Type": "application/json" 102 | }), 103 | body: JSON.stringify(item) 104 | }) 105 | 106 | response = await response.json() 107 | console.log(item.title, response.result ? 'Added!' : 'Something went wrong...') 108 | } 109 | 110 | /** 111 | * Loop the array and dispatch the post requests 112 | * 113 | * @param {[{collectionId: number, title: string, link: string, tags: [string], created: any, description: string}]} list 114 | * @param {number} [number=1] 115 | */ 116 | async function loopEntriesList(list, number = 1) { 117 | console.log(`===== Block #${number} start =====`) 118 | list.forEach(async entry => { 119 | try { 120 | await createNewRainDrop(entry) 121 | } catch (error) { 122 | console.log(error) 123 | } 124 | }); 125 | } 126 | 127 | /** 128 | * Create a multiple entries via a loop of single post requests 129 | */ 130 | async function createNewRainDrops() { 131 | const dataFile = readFileSync('./newData.json', { encoding: 'utf-8' }) 132 | const parsed = JSON.parse(dataFile) 133 | 134 | for (const [index, item] of parsed.entries()) { 135 | const time = index === 0 ? 0 : index * 1.5 136 | 137 | setTimeout(async () => { 138 | await loopEntriesList(item, index + 1) 139 | }, 60000 * time); 140 | } 141 | } 142 | 143 | /** 144 | * Create multiple entries at a time (max. supported 100 entries) 145 | * Note: It doesn't do the auto fetch of the description! 146 | */ 147 | async function createNewRainDropsMultiple() { 148 | const fetchUrl = 'https://api.raindrop.io/rest/v1/raindrops' 149 | const dataFile = readFileSync('./newData.json', { encoding: 'utf-8' }) 150 | const parsed = JSON.parse(dataFile) 151 | 152 | for (const item of parsed) { 153 | const response = await fetch(fetchUrl, { 154 | method: "POST", 155 | headers: new Headers({ 156 | "Authorization": `Bearer ${process.env.RAINDROP_TOKEN}`, 157 | "Content-Type": "application/json" 158 | }), 159 | body: JSON.stringify({ items: item }) 160 | }).then(res => res.json()); 161 | 162 | console.log(item.length, response.result ? 'Added!' : 'Something wrong happened...') 163 | } 164 | } 165 | 166 | // 1 - Fist run this function... 167 | // getGithubStarsList('https://api.github.com/users/azedo/starred'); 168 | 169 | // 2 - Then run this one... 170 | // readAndParseGithubData() 171 | 172 | // 3 - And lastly, this one! (Choose the one that you prefer!) 173 | // createNewRainDrops({ 174 | // collectionId: number, 175 | // title: string, 176 | // link: string, 177 | // tags: [string], 178 | // created: Date, 179 | // excerpt: string 180 | // }) // Single 181 | // createNewRainDrops() // Multiple by single loop 182 | // createNewRainDropsMultiple() // Multiple by 100's 183 | 184 | // 4 - Now check your raindrops.io account and all the bookmarks should be there! ;) 185 | // https://app.raindrop.io 186 | 187 | // Note: For API documentation for raindrop.io -> https://developer.raindrop.io/ 188 | --------------------------------------------------------------------------------