├── .gitignore ├── LICENSE ├── README.rst ├── backup_instapaper.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | instapaper_bookmarks.json 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | backup-instapaper 2 | ================= 3 | 4 | This is a script for backing up your bookmarks from Instapaper. 5 | 6 | It mimics the `HTML/CSV export function 7 | `_ 8 | provided by Instapaper, but in script form. 9 | 10 | Installation 11 | ************ 12 | 13 | To install this script, use pip: 14 | 15 | .. code-block:: console 16 | 17 | $ pip install -e git+git://github.com/alexwlchan/backup-instapaper.git#egg=backup_instapaper 18 | 19 | or `pipsi `_: 20 | 21 | .. code-block:: console 22 | 23 | $ pipsi install -e git+git://github.com/alexwlchan/backup-instapaper.git#egg=backup_instapaper 24 | 25 | 26 | You can use Python 2.7 and Python 3.3+. 27 | 28 | You also need to get an OAuth key/secret for the Instapaper API. You can 29 | register for these `on the Instapaper website `_. 30 | 31 | Usage 32 | ***** 33 | 34 | Run the script, passing your username, password, and API keys as command-line 35 | flags: 36 | 37 | .. code-block:: console 38 | 39 | $ backup_instapaper --username=USERNAME --password=PASSWORD --oauthkey=OAUTHKEY --oauthsec=OAUTHSEC 40 | 41 | This will write your bookmarks to ``instapaper_bookmarks.json``. 42 | 43 | For all the options, use the ``--help`` flag: 44 | 45 | .. code-block:: console 46 | 47 | $ backup_instapaper --help 48 | 49 | License 50 | ******* 51 | 52 | This script is licensed under the MIT license. 53 | -------------------------------------------------------------------------------- /backup_instapaper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 3 | """A script for backing up your Instapaper bookmarks. Run with ``--help`` 4 | to get a usage message. 5 | 6 | """ 7 | 8 | from __future__ import unicode_literals 9 | 10 | import argparse 11 | import json 12 | 13 | from instapaper import Instapaper 14 | import keyring 15 | 16 | 17 | def _bookmark_to_dict(bookmark): 18 | """Turns a bookmark into a dictionary. 19 | 20 | :param bookmark: An ``instapaper.Bookmark`` instance. 21 | 22 | """ 23 | data = {} 24 | for field in ['bookmark_id', 'title', 'URL', 'description', 'starred']: 25 | data[field] = getattr(bookmark, field) 26 | return data 27 | 28 | 29 | def fetch_all_bookmarks(api): 30 | """Fetches all the bookmarks for an account. 31 | 32 | Returns a dict of folder names and bookmarks. 33 | 34 | :param api: An ``Instapaper`` instance logged in to the Instapaper API. 35 | 36 | """ 37 | # We have to retrieve the bookmarks from each folder in an account 38 | # individually, so start by getting a list of folders. 39 | folders = {f['folder_id']: f['title'] for f in api.folders()} 40 | 41 | # Unread and Archive aren't included in the ``folders()`` method, 42 | # but we still want to back them up. 43 | folders['archive'] = 'Archive' 44 | folders['unread'] = 'Unread' 45 | 46 | bookmarks = [] 47 | for f_id, f_title in folders.items(): 48 | new_bookmarks = _fetch_bookmarks_for_folder(api=api, folder_id=f_id) 49 | for b in new_bookmarks: 50 | b['folder'] = f_title 51 | bookmarks.append(b) 52 | 53 | return bookmarks 54 | 55 | 56 | def _fetch_bookmarks_for_folder(api, folder_id): 57 | """Fetches all the bookmarks for a given folder. 58 | 59 | :param api: An ``Instapaper`` instance logged in to the Instapaper API. 60 | :param folder_id: The folder ID, as returned by the ``folders`` method 61 | from the Instapaper API, or "unread" or "archive". 62 | 63 | """ 64 | bookmarks = [] 65 | while True: 66 | # Fetch the next batch of bookmarks from the API. 500 is the most 67 | # bookmarks we can fetch at once -- this lets us minimise API calls. 68 | new_bookmarks = api.bookmarks( 69 | folder=folder_id, 70 | have=','.join(b['bookmark_id'] for b in bookmarks), 71 | limit=500) 72 | 73 | # Add any new bookmarks. Because we pass the ``have`` parameter, 74 | # the API guarantees that we aren't receiving any duplicates. 75 | new_bookmarks = [_bookmark_to_dict(b) for b in new_bookmarks] 76 | bookmarks.extend(new_bookmarks) 77 | 78 | # The next API call will be empty if there aren't any bookmarks left, 79 | # but checking here can save us an unnecessary API call. 80 | if len(new_bookmarks) < 500: 81 | break 82 | 83 | return bookmarks 84 | 85 | 86 | def read_config(): 87 | """Returns configuration for using the script. 88 | 89 | Configuration is read from one of two places: 90 | 1. The system keychain 91 | 2. Command-line arguments 92 | 93 | Command-line arguments take precedence over keychain values. If the 94 | keychain values are empty/missing, the command-line switches are required. 95 | 96 | """ 97 | # Read some initial config from the system keychain: if this doesn't 98 | # exist, then we require it from command-line arguments. 99 | username = keyring.get_password('instapaper', 'username') 100 | password = keyring.get_password('instapaper', 'password') 101 | oauthkey = keyring.get_password('instapaper', 'oauthkey') 102 | oauthsec = keyring.get_password('instapaper', 'oauthsec') 103 | 104 | parser = argparse.ArgumentParser( 105 | description='A script to back up bookmarks from Instapaper', 106 | epilog='This script requires API keys for Instapaper. You can get an ' 107 | 'API key from ' 108 | 'https://www.instapaper.com/main/request_oauth_consumer_token') 109 | 110 | parser.add_argument( 111 | '--output', default='instapaper_bookmarks.json', 112 | help='output path for the backup file') 113 | parser.add_argument( 114 | '--username', required=(username is None), 115 | help='Instapaper username') 116 | parser.add_argument( 117 | '--password', required=(password is None), 118 | help='Instapaper password') 119 | parser.add_argument( 120 | '--oauthkey', required=(oauthkey is None), 121 | help='OAuth key for the Instapaper API') 122 | parser.add_argument( 123 | '--oauthsec', required=(oauthsec is None), 124 | help='OAuth secret for the Instapaper API') 125 | 126 | config = vars(parser.parse_args()) 127 | 128 | if config['username'] is None: 129 | config['username'] = username 130 | if config['password'] is None: 131 | config['password'] = password 132 | if config['oauthkey'] is None: 133 | config['oauthkey'] = oauthkey 134 | if config['oauthsec'] is None: 135 | config['oauthsec'] = oauthsec 136 | 137 | return config 138 | 139 | 140 | def setup_api(username, password, oauthkey, oauthsec): 141 | """Set up an instance of the Instapaper API. 142 | 143 | :param username: Instapaper username 144 | :param password: Instapaper password 145 | :param oauthkey: OAuth key for the Instapaper API 146 | :param oauthsec: OAuth secret for the Instapaper API 147 | 148 | """ 149 | api = Instapaper(oauthkey=oauthkey, oauthsec=oauthsec) 150 | api.login(username=username, password=password) 151 | return api 152 | 153 | 154 | def main(): 155 | """Use the Instapaper API to save bookmarks to disk.""" 156 | config = read_config() 157 | api = setup_api( 158 | username=config['username'], 159 | password=config['password'], 160 | oauthkey=config['oauthkey'], 161 | oauthsec=config['oauthsec']) 162 | bookmarks = fetch_all_bookmarks(api) 163 | json.dump(bookmarks, open(config['output'], 'w'), sort_keys=True) 164 | 165 | 166 | if __name__ == '__main__': 167 | main() 168 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | import codecs 5 | import os 6 | 7 | from setuptools import find_packages, setup 8 | 9 | 10 | def local_file(name): 11 | return os.path.relpath(os.path.join(os.path.dirname(__file__), name)) 12 | 13 | 14 | README = local_file('README.rst') 15 | long_description = codecs.open(README, encoding='utf-8').read() 16 | 17 | 18 | setup( 19 | name='backup_instapaper', 20 | version='1.0.0', 21 | description='A script for backing up bookmarks from your Instapaper account', 22 | long_description=long_description, 23 | url='https://github.com/alexwlchan/backup-instapaper', 24 | author='Alex Chan', 25 | author_email='alex@alexwlchan.net', 26 | license='MIT', 27 | classifiers=[ 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Intended Audience :: Other Audience', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.3', 35 | 'Programming Language :: Python :: 3.4', 36 | 'Programming Language :: Python :: 3.5', 37 | 'Programming Language :: Python :: 3.6', 38 | ], 39 | install_requires=[ 40 | 'instapaper>=0.3.1', 41 | 'keyring>=10.2,<11', 42 | # only required because the version of instapaper on PyPI doesn't 43 | # have oauth2 in its setup.py 44 | 'oauth2>=1.9.0post1,<2', 45 | ], 46 | entry_points={ 47 | 'console_scripts': [ 48 | 'backup_instapaper=backup_instapaper:main', 49 | ], 50 | }, 51 | ) 52 | --------------------------------------------------------------------------------