├── config.txt ├── LICENSE ├── README.md └── main.py /config.txt: -------------------------------------------------------------------------------- 1 | [Readability] 2 | user= 3 | password= 4 | key= 5 | secret= 6 | 7 | [Pocket] 8 | user= 9 | password= 10 | key= 11 | 12 | [PinBoard] 13 | user= 14 | password= 15 | token= 16 | 17 | [Delicious] 18 | user= 19 | password= 20 | 21 | [Instapaper] 22 | user= 23 | password= 24 | key= 25 | secret= 26 | 27 | [Diigo] 28 | user= 29 | password= 30 | key= 31 | 32 | [StackOverflow] 33 | # user is the numberic value that appears in the url when you view your profile page, eg '12345' in http://stackoverflow.com/users/12345/ironman 34 | user= 35 | 36 | [Github] 37 | user= 38 | 39 | [Twitter] 40 | user= 41 | api_key= 42 | api_secret= 43 | access_token= 44 | access_token_secret= 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rob Dawson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reading List Mover 2 | 3 | Copy bookmarks between Instapaper, Readability, Pocket, Pinboard, Delicious, Diigo, GitHub, StackOverflow and Twitter. 4 | 5 | Here's a small Python library to copy bookmarks/favourites between a number of online services. The library supports [Instapaper](http://www.instapaper.com/), [Readability](https://www.readability.com/), [Pocket](http://getpocket.com/) (formerly ReadItLater), [Pinboard](http://pinboard.in/), [Delicious](http://delicious.com/), [Diigo](http://diigo.com/), [GitHub](http://github.com/)\*, [StackOverflow](http://stackoverflow.com/)\* and [Twitter](http://twitter.com/)\* ('\*' indicates export only). 6 | 7 | To use the library you will need to populate your copy of the [config.txt](https://github.com/codebox/reading-list-mover/blob/master/config.txt) file with the details of your accounts on the services that you are going to use. In the case of Readability, Pocket, Instapaper, Diigo and Twitter you will also need to apply for an API key: 8 | 9 | - [Readability API key](http://help.readability.com/customer/portal/articles/267466-i%E2%80%99m-a-developer-how-can-i-get-an-api-key-) 10 | - [Pocket API key](http://getpocket.com/api/signup/) 11 | - [Instapaper API key](http://www.instapaper.com/main/request_oauth_consumer_token) 12 | - [Diigo API key](http://www.diigo.com/api_keys/new/) 13 | - [Twitter API key](https://apps.twitter.com/app/new) 14 | 15 | You will also need to have the [oauth2](https://github.com/simplegeo/python-oauth2) Python library installed on your system. 16 | 17 | Here's an example showing how to copy all your bookmarks from one service to another: 18 | 19 | ## Copy from Pocket to Readability ## 20 | 21 | ```python 22 | # Copy all bookmarks from Pocket to Readability 23 | pocket = buildPocket() 24 | readability = buildReadability() 25 | 26 | for b in pocket.getBookmarks(): 27 | readability.addBookmark(b) 28 | ``` 29 | 30 | You can also use the library to export a list of your bookmarks for use as a backup: 31 | 32 | ## Export all Delicious bookmarks 33 | 34 | ```python 35 | # Print out all bookmarks from Delicious 36 | delicious = buildDelicious() 37 | 38 | for b in delicious.getBookmarks(): 39 | print b['title'] + ': ' + b['url'] 40 | ``` 41 | 42 | [source]: https://github.com/codebox/reading-list-mover 43 | [Instapaper]: http://www.instapaper.com/ 44 | [Readability]: https://www.readability.com/ 45 | [Pocket]: http://getpocket.com/ 46 | [Pinboard]: http://pinboard.in/ 47 | [Delicious]: http://delicious.com/ 48 | [Diigo]: http://diigo.com/ 49 | [GitHub]: http://github.com/ 50 | [StackOverflow]: http://stackoverflow.com/ 51 | [Twitter]: http://twitter.com/ 52 | [config.txt]: https://github.com/codebox/reading-list-mover/blob/master/config.txt 53 | [oauth2]: https://github.com/simplegeo/python-oauth2 54 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import urllib 4 | import urllib2 5 | import urlparse 6 | import simplejson 7 | from xml.dom.minidom import parseString 8 | import xml.dom.minidom 9 | import oauth2 10 | import ConfigParser 11 | from StringIO import StringIO 12 | import gzip 13 | 14 | class OAuthClient: 15 | def __init__(self, key, secret, user, password): 16 | consumer = oauth2.Consumer(key, secret) 17 | client = oauth2.Client(consumer) 18 | resp, content = client.request(self.token_url, "POST", urllib.urlencode({ 19 | 'x_auth_mode': 'client_auth', 20 | 'x_auth_username': user, 21 | 'x_auth_password': password 22 | })) 23 | token = dict(urlparse.parse_qsl(content)) 24 | token = oauth2.Token(token['oauth_token'], token['oauth_token_secret']) 25 | self.http = oauth2.Client(consumer, token) 26 | 27 | def getBookmarks(self): 28 | response, data = self.http.request(self.get_url, method='GET') 29 | bookmarks = [] 30 | 31 | for b in simplejson.loads(data)['bookmarks']: 32 | article = b['article'] 33 | bookmarks.append({'url' : article['url'], 'title' : article['title']}) 34 | 35 | return bookmarks 36 | 37 | def addBookmark(self, bookmark): 38 | self.http.request(self.add_url, method='POST', body=urllib.urlencode({ 39 | 'url': bookmark['url'], 40 | 'title': bookmark['title'].encode('utf-8') 41 | })) 42 | 43 | class Readability(OAuthClient): 44 | def __init__(self, key, secret, user, password): 45 | self.token_url = 'https://www.readability.com/api/rest/v1/oauth/access_token/' 46 | self.get_url = 'https://www.readability.com/api/rest/v1/bookmarks' 47 | self.add_url = 'https://www.readability.com/api/rest/v1/bookmarks' 48 | 49 | OAuthClient.__init__(self, key, secret, user, password) 50 | 51 | class Instapaper(OAuthClient): 52 | def __init__(self, key, secret, user, password): 53 | self.token_url = 'https://www.instapaper.com/api/1/oauth/access_token' 54 | self.get_url = 'https://www.instapaper.com/api/1/bookmarks/list' 55 | self.add_url = 'https://www.instapaper.com/api/1/bookmarks/add' 56 | 57 | OAuthClient.__init__(self, key, secret, user, password) 58 | 59 | def getBookmarks(self): 60 | ''' 61 | The ability to export bookmarks from Instapaper is reserved for users with Subscription accounts, if you have 62 | such an account and wish to enable this feature just delete this function 63 | ''' 64 | raise Exception('Not supported') 65 | 66 | class HttpAuthClient: 67 | def __init__(self, user, password): 68 | passman = urllib2.HTTPPasswordMgrWithDefaultRealm() 69 | passman.add_password(None, self.get_url, user, password) 70 | passman.add_password(None, self.add_url, user, password) 71 | authhandler = urllib2.HTTPBasicAuthHandler(passman) 72 | self.url_opener = urllib2.build_opener(authhandler) 73 | 74 | def open(self, url, data=None): 75 | return self.url_opener.open(url, data) 76 | 77 | class StackOverflow: 78 | def __init__(self, user): 79 | self.get_url = 'http://api.stackexchange.com/2.1/users/' + user + '/favorites?order=desc&sort=activity&site=stackoverflow' 80 | 81 | def getBookmarks(self): 82 | rsp = urllib2.urlopen(self.get_url) 83 | if rsp.info().get('Content-Encoding') == 'gzip': 84 | buf = StringIO(rsp.read()) 85 | rsp = gzip.GzipFile(fileobj=buf) 86 | 87 | data = json.load(rsp) 88 | return [{'url' : b['link'], 'title' : b['title']} for b in data['items']] 89 | 90 | def addBookmark(self, bookmark): 91 | raise Exception('Not supported') 92 | 93 | class Github: 94 | def __init__(self, user): 95 | self.get_url = 'https://api.github.com/users/' + user + '/starred' 96 | 97 | def getBookmarks(self): 98 | rsp = urllib2.urlopen(self.get_url) 99 | data = json.load(rsp) 100 | return [{'url' : b['url'], 'title' : b['name']} for b in data] 101 | 102 | def addBookmark(self, bookmark): 103 | raise Exception('Not supported') 104 | 105 | class Twitter: 106 | def __init__(self, user, api_key, api_secret, access_token, access_token_secret): 107 | self.get_url = "https://api.twitter.com/1.1/favorites/list.json?screen_name=" + user 108 | self.tweet_url_prefix = "https://twitter.com/" + user + "/status/" 109 | consumer = oauth2.Consumer(api_key, api_secret) 110 | token = oauth2.Token(access_token, access_token_secret) 111 | self.http = oauth2.Client(consumer, token) 112 | 113 | def getBookmarks(self): 114 | response, data = self.http.request(self.get_url, method='GET') 115 | bookmarks = [] 116 | 117 | for b in simplejson.loads(data): 118 | bookmarks.append({'url' : self.tweet_url_prefix + b['id_str'], 'title' : b['text']}) 119 | 120 | return bookmarks 121 | 122 | def addBookmark(self, bookmark): 123 | raise Exception('Not supported') 124 | 125 | class Diigo(HttpAuthClient): 126 | def __init__(self, user, password, key): 127 | self.get_url = 'https://secure.diigo.com/api/v2/bookmarks?key=' + key + '&user=' + user 128 | self.add_url = 'https://secure.diigo.com/api/v2/bookmarks' 129 | self.key = key 130 | HttpAuthClient.__init__(self, user, password) 131 | 132 | def getBookmarks(self): 133 | data = json.load(self.open(self.get_url)) 134 | return [{'url' : b['url'], 'title' : b['title']} for b in data] 135 | 136 | def addBookmark(self, bookmark): 137 | add_args=urllib.urlencode({'url' : bookmark['url'], 'title' : bookmark['title'], 'key' : self.key, 'shared' : 'yes'}) 138 | self.open(self.add_url, add_args) 139 | ''' 140 | During testing the Diigo service sometimes returned a '500 Server error' when adding lots of bookmarks in rapid succession, adding 141 | a brief pause between 'add' operations seemed to fix it - YMMV 142 | time.sleep(1) 143 | ''' 144 | 145 | class DeliciousLike(HttpAuthClient): 146 | def __init__(self, user, password): 147 | HttpAuthClient.__init__(self, user, password) 148 | 149 | def getBookmarks(self): 150 | xml = self.open(self.get_url).read() 151 | dom = parseString(xml) 152 | 153 | urls = [] 154 | for n in dom.firstChild.childNodes: 155 | if n.nodeType == n.ELEMENT_NODE: 156 | urls.append({'url' : n.getAttribute('href'), 'title' : n.getAttribute('description')}) 157 | 158 | return urls 159 | 160 | def addBookmark(self, bookmark): 161 | params = urllib.urlencode({'url' : bookmark['url'], 'description' : bookmark['title'].encode('utf-8')}) 162 | self.open(self.add_url + params) 163 | 164 | class PinBoard(DeliciousLike): 165 | def __init__(self, user, password): 166 | self.get_url = 'https://api.pinboard.in/v1/posts/all' 167 | self.add_url = 'https://api.pinboard.in/v1/posts/add?' 168 | 169 | DeliciousLike.__init__(self, user, password) 170 | 171 | class PinBoard2(DeliciousLike): 172 | def __init__(self, user, token): 173 | auth_token = user + ':' + token 174 | self.get_url = 'https://api.pinboard.in/v1/posts/all?auth_token=' + auth_token 175 | self.add_url = 'https://api.pinboard.in/v1/posts/add?auth_token=' + auth_token + '&' 176 | 177 | def open(self, url, data=None): 178 | return urllib2.urlopen(url, data) 179 | 180 | class Delicious(DeliciousLike): 181 | def __init__(self, user, password): 182 | self.get_url = 'https://api.del.icio.us/v1/posts/all' 183 | self.add_url = 'https://api.del.icio.us/v1/posts/add?' 184 | 185 | DeliciousLike.__init__(self, user, password) 186 | 187 | class Pocket: 188 | def __init__(self, user, password, key): 189 | base_args=urllib.urlencode({'username' : user, 'password' : password, 'apikey' : key}) 190 | self.get_url = 'https://readitlaterlist.com/v2/get?' + base_args + '&' 191 | self.add_url = 'https://readitlaterlist.com/v2/add?' + base_args + '&' 192 | 193 | def getBookmarks(self): 194 | get_args=urllib.urlencode({'state' : 'unread'}) 195 | data = json.load(urllib2.urlopen(self.get_url + get_args)) 196 | return [{'url' : b['url'], 'title' : b['title']} for b in data['list'].values()] 197 | 198 | def addBookmark(self, bookmark): 199 | add_args=urllib.urlencode({'url' : bookmark['url']}) 200 | urllib2.urlopen(self.add_url + add_args) 201 | 202 | config = ConfigParser.RawConfigParser() 203 | config.read('config.txt') 204 | 205 | def buildReadability(): 206 | SECTION = 'Readability' 207 | return Readability(config.get(SECTION, 'key'), config.get(SECTION, 'secret'), config.get(SECTION, 'user'), config.get(SECTION, 'password')) 208 | 209 | def buildPocket(): 210 | SECTION = 'Pocket' 211 | return Pocket(config.get(SECTION, 'user'), config.get(SECTION, 'password'), config.get(SECTION, 'key')) 212 | 213 | def buildPinBoard(): 214 | SECTION = 'PinBoard' 215 | return PinBoard(config.get(SECTION, 'user'), config.get(SECTION, 'password')) 216 | 217 | def buildPinBoard2(): 218 | SECTION = 'PinBoard' 219 | return PinBoard2(config.get(SECTION, 'user'), config.get(SECTION, 'token')) 220 | 221 | def buildDelicious(): 222 | SECTION = 'Delicious' 223 | return Delicious(config.get(SECTION, 'user'), config.get(SECTION, 'password')) 224 | 225 | def buildInstapaper(): 226 | SECTION = 'Instapaper' 227 | return Instapaper(config.get(SECTION, 'key'), config.get(SECTION, 'secret'), config.get(SECTION, 'user'), config.get(SECTION, 'password')) 228 | 229 | def buildDiigo(): 230 | SECTION = 'Diigo' 231 | return Diigo(config.get(SECTION, 'user'), config.get(SECTION, 'password'), config.get(SECTION, 'key')) 232 | 233 | def buildStackOverflow(): 234 | SECTION = 'StackOverflow' 235 | return StackOverflow(config.get(SECTION, 'user')) 236 | 237 | def buildGithub(): 238 | SECTION = 'Github' 239 | return Github(config.get(SECTION, 'user')) 240 | 241 | def buildTwitter(): 242 | SECTION = 'Twitter' 243 | return Twitter(config.get(SECTION, 'user'), config.get(SECTION, 'api_key'), config.get(SECTION, 'api_secret'), config.get(SECTION, 'access_token'), config.get(SECTION, 'access_token_secret')) 244 | --------------------------------------------------------------------------------