├── .github └── workflows │ └── build.yaml ├── LICENSE ├── NOTICE ├── README.md └── src ├── __init__.py ├── about.txt ├── config.py ├── images └── icon.png ├── main.py ├── plugin-import-name-readwise.txt └── ui.py /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | tags: 6 | - "v*" 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - run: zip -r ../calibre-plugin-readwise.zip * 13 | working-directory: src 14 | - run: zip calibre-plugin-readwise.zip COPYING README.md 15 | - uses: actions/upload-artifact@v2 16 | with: 17 | name: calibre-plugin-readwise 18 | path: calibre-plugin-readwise.zip 19 | - uses: actions/create-release@v1 20 | id: create_release 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | tag_name: ${{ github.ref }} 25 | release_name: Readwise Plugin for Calibre ${{ github.ref }} 26 | draft: false 27 | prerelease: false 28 | if: startsWith(github.ref, 'refs/tags/') 29 | - uses: actions/upload-release-asset@v1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | with: 33 | upload_url: ${{ steps.create_release.outputs.upload_url }} 34 | asset_path: ./calibre-plugin-readwise.zip 35 | asset_name: calibre-plugin-readwise.zip 36 | asset_content_type: application/zip 37 | if: startsWith(github.ref, 'refs/tags/') 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 William Bartholomew 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 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | The Readwise logo in src/images/icon.png is used with permission from readwise.io 2 | and is not covered by the license of this repository. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Readwise Plugin for Calibre 2 | [Calibre](https://calibre-ebook.com/) plugin to export your highlights to [Readwise](https://readwise.io/). This plugin is **not** supported by Readwise - please report any issues in this repository. 3 | 4 | ![957445CE-FDFC-4E4C-ADE2-EDE8EDD35A51](https://user-images.githubusercontent.com/3266447/107865551-7d952c80-6e1c-11eb-9ff3-3b8586defd68.GIF) 5 | 6 | ## Requirements 7 | * [Calibre](https://calibre-ebook.com/) >= 5.0.1 8 | * [Readwise](https://readwise.io) account 9 | 10 | ## Installation 11 | 1. Install the Calibre plugin: 12 | 1. Download the [latest release](releases/latest). 13 | 1. Open Calibre. 14 | 1. Click Preferences and then Plugins. 15 | 1. Click Load Plugin From File and then select the release you downloaded above. 16 | 1. Get a Readwise access token by going to https://readwise.io/access_token. 17 | 1. Configure the plugin: 18 | 1. Click the Readwise icon on the main toolbar. 19 | 1. Click Configure This Plugin. 20 | 1. Paste the access token and click OK. 21 | 22 | ## Usage 23 | Highlights are not exported to Readwise automatically, to trigger the export: 24 | 25 | 1. Click the Readwise icon on the main toolbar. 26 | 1. Click the Export to Readwise button. 27 | 28 | Deleted highlights will **not** be deleted from Readwise, if you want to remove them you need to do that within Readwise. 29 | 30 | ## License 31 | MIT - see [LICENSE](LICENSE). Readwise logo used with permission - see [NOTICE](NOTICE). 32 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from calibre.customize import InterfaceActionBase 2 | 3 | class ReadwisePlugin(InterfaceActionBase): 4 | name = 'Readwise' 5 | description = 'Export highlights to Readwise' 6 | supported_platforms = ['windows', 'osx', 'linux'] 7 | author = 'William Bartholomew' 8 | version = (0, 1, 1) 9 | minimum_calibre_version = (5, 0, 1) 10 | actual_plugin = 'calibre_plugins.readwise.ui:InterfacePlugin' 11 | 12 | def is_customizable(self): 13 | return True 14 | 15 | def config_widget(self): 16 | from calibre_plugins.readwise.config import ConfigWidget 17 | return ConfigWidget() 18 | 19 | def save_settings(self, config_widget): 20 | config_widget.save_settings() 21 | ac = self.actual_plugin_ 22 | if ac is not None: 23 | ac.apply_settings() 24 | -------------------------------------------------------------------------------- /src/about.txt: -------------------------------------------------------------------------------- 1 | Readwise Plugin for Calibre 2 | Copyright (c) 2021 William Bartholomew 3 | 4 | Calibre plugin to export your highlights to Readwise. 5 | 6 | This plugin is NOT supported by Readwise - please report any issues at https://github.com/iamwillbar/calibre-plugin-readwise. 7 | 8 | Licensed under the MIT license. Readwise logo used with permission. 9 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | from calibre.utils.config import JSONConfig 2 | from PyQt5.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit 3 | 4 | prefs = JSONConfig('plugins/readwise') 5 | prefs.defaults['access_token'] = '' 6 | 7 | class ConfigWidget(QWidget): 8 | def __init__(self): 9 | QWidget.__init__(self) 10 | self.l = QHBoxLayout() 11 | self.setLayout(self.l) 12 | 13 | self.label = QLabel('Access &token:') 14 | self.l.addWidget(self.label) 15 | self.at = QLineEdit(self) 16 | self.at.setEchoMode(QLineEdit.Password) 17 | self.at.setText(prefs['access_token']) 18 | self.l.addWidget(self.at) 19 | self.label.setBuddy(self.at) 20 | self.access_token_link_label = QLabel('Get access token') 21 | self.access_token_link_label.setOpenExternalLinks(True) 22 | self.l.addWidget(self.access_token_link_label) 23 | 24 | def save_settings(self): 25 | prefs['access_token'] = self.at.text() 26 | -------------------------------------------------------------------------------- /src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamwillbar/calibre-plugin-readwise/d2f421380feaa4826d73393fbf4e82c7d57e306a/src/images/icon.png -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | from calibre_plugins.readwise.config import prefs 2 | from PyQt5.Qt import QDialog, QVBoxLayout, QLabel, QPushButton, QMessageBox 3 | import urllib.request 4 | import json 5 | 6 | class ReadwiseDialog(QDialog): 7 | def __init__(self, gui, icon, do_user_config): 8 | QDialog.__init__(self, gui) 9 | self.gui = gui 10 | self.do_user_config = do_user_config 11 | 12 | self.db = gui.current_db 13 | 14 | self.l = QVBoxLayout() 15 | self.setLayout(self.l) 16 | 17 | self.setWindowTitle('Readwise') 18 | self.setWindowIcon(icon) 19 | 20 | self.about_button = QPushButton('&About', self) 21 | self.about_button.clicked.connect(self.about) 22 | self.l.addWidget(self.about_button) 23 | 24 | self.sync_button = QPushButton( 25 | '&Export to Readwise', self) 26 | self.sync_button.clicked.connect(self.sync) 27 | self.l.addWidget(self.sync_button) 28 | 29 | self.conf_button = QPushButton( 30 | '&Configure this plugin', self) 31 | self.conf_button.clicked.connect(self.config) 32 | self.l.addWidget(self.conf_button) 33 | 34 | self.resize(self.sizeHint()) 35 | self.update_button_state() 36 | 37 | def about(self): 38 | text = get_resources('about.txt') 39 | QMessageBox.about(self, 'About the Readwise plugin', 40 | text.decode('utf-8')) 41 | 42 | def sync(self): 43 | db = self.db.new_api 44 | 45 | books = {} 46 | for annotation in db.all_annotations(None, None, 'highlight', True, None): 47 | books.setdefault(annotation['book_id'], []).append(annotation) 48 | 49 | if len(books) == 0: 50 | QMessageBox.information(self, "Readwise", "There are no highlights to export.") 51 | return 52 | 53 | body = { 54 | 'highlights': [] 55 | } 56 | for book_id, annotations in books.items(): 57 | metadata = db.get_metadata(book_id) 58 | for annotation in annotations: 59 | highlight = { 60 | 'text': annotation['annotation']['highlighted_text'], 61 | 'title': metadata.title, 62 | 'author': metadata.authors[0], 63 | 'source_type': 'book', 64 | 'note': annotation['annotation'].get('notes', None), 65 | 'highlighted_at': annotation['annotation']['timestamp'] 66 | } 67 | body['highlights'].append(highlight) 68 | 69 | headers = { 70 | 'Authorization': f"Token {prefs['access_token']}", 71 | 'Content-Type': 'application/json' 72 | } 73 | request = urllib.request.Request('https://readwise.io/api/v2/highlights/', json.dumps(body).encode('utf-8'), headers = headers) 74 | 75 | try: 76 | if self.gui: 77 | self.gui.status_bar.showMessage('Exporting to Readwise...') 78 | 79 | response = urllib.request.urlopen(request) 80 | QMessageBox.information(self, "Readwise", "Export completed successfully.") 81 | 82 | 83 | except urllib.error.HTTPError as e: 84 | if e.code == 401: 85 | QMessageBox.critical(self, "Readwise", "Export failed due to incorrect access token. Please update the access token and try again.") 86 | else: 87 | QMessageBox.critical(self, "Readwise", f"Export failed with status code: {e.code}") 88 | 89 | except urllib.error.URLError as e: 90 | QMessageBox.critical(self, "Readwise", f"Export failed with reason: {e.reason}") 91 | 92 | finally: 93 | if self.gui: 94 | self.gui.status_bar.clearMessage() 95 | 96 | def config(self): 97 | self.do_user_config(parent=self) 98 | self.update_button_state() 99 | 100 | def update_button_state(self): 101 | self.sync_button.setEnabled(len(prefs['access_token']) > 0) 102 | -------------------------------------------------------------------------------- /src/plugin-import-name-readwise.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamwillbar/calibre-plugin-readwise/d2f421380feaa4826d73393fbf4e82c7d57e306a/src/plugin-import-name-readwise.txt -------------------------------------------------------------------------------- /src/ui.py: -------------------------------------------------------------------------------- 1 | from calibre.gui2.actions import InterfaceAction 2 | from calibre_plugins.readwise.main import ReadwiseDialog 3 | 4 | class InterfacePlugin(InterfaceAction): 5 | name = 'Readwise' 6 | action_spec = ('Readwise', None, 'Export highlights to Readwise', None) 7 | 8 | def genesis(self): 9 | icon = get_icons('images/icon.png') 10 | self.qaction.setIcon(icon) 11 | self.qaction.triggered.connect(self.show_dialog) 12 | 13 | def show_dialog(self): 14 | base_plugin_object = self.interface_action_base_plugin 15 | do_user_config = base_plugin_object.do_user_config 16 | d = ReadwiseDialog(self.gui, self.qaction.icon(), do_user_config) 17 | d.show() 18 | 19 | def apply_settings(self): 20 | from calibre_plugins.readwise.config import prefs 21 | prefs 22 | --------------------------------------------------------------------------------