1__license__ = 'GPL v3' 2__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' 3 4import re, ssl, json 5from threading import Thread, Event 6 7from qt.core import (QObject, pyqtSignal, Qt, QUrl, QDialog, QGridLayout, 8 QLabel, QCheckBox, QDialogButtonBox, QIcon) 9 10from calibre.constants import (__appname__, __version__, iswindows, ismacos, 11 isportable, is64bit, numeric_version) 12from calibre import prints, as_unicode 13from calibre.utils.config import prefs 14from calibre.utils.localization import localize_website_link 15from calibre.utils.https import get_https_resource_securely 16from calibre.gui2 import config, dynamic, open_url 17from calibre.gui2.dialogs.plugin_updater import get_plugin_updates_available 18from calibre.utils.serialize import msgpack_dumps, msgpack_loads 19from polyglot.binary import as_hex_unicode, from_hex_bytes 20 21URL = 'https://code.calibre-ebook.com/latest' 22# URL = 'http://localhost:8000/latest' 23NO_CALIBRE_UPDATE = (0, 0, 0) 24 25 26def get_download_url(): 27 which = ('portable' if isportable else 'windows' if iswindows 28 else 'osx' if ismacos else 'linux') 29 if which == 'windows' and is64bit: 30 which += '64' 31 return localize_website_link('https://calibre-ebook.com/download_' + which) 32 33 34def get_newest_version(): 35 try: 36 icon_theme_name = json.loads(I('icon-theme.json', data=True))['name'] 37 except Exception: 38 icon_theme_name = '' 39 headers={ 40 'CALIBRE-VERSION':__version__, 41 'CALIBRE-OS': ('win' if iswindows else 'osx' if ismacos else 'oth'), 42 'CALIBRE-INSTALL-UUID': prefs['installation_uuid'], 43 'CALIBRE-ICON-THEME': icon_theme_name, 44 } 45 try: 46 version = get_https_resource_securely(URL, headers=headers) 47 except ssl.SSLError as err: 48 if getattr(err, 'reason', None) != 'CERTIFICATE_VERIFY_FAILED': 49 raise 50 # certificate verification failed, since the version check contains no 51 # critical information, ignore and proceed 52 # We have to do this as if the calibre CA certificate ever 53 # needs to be revoked, then we won't be able to do version checks 54 version = get_https_resource_securely(URL, headers=headers, cacerts=None) 55 try: 56 version = version.decode('utf-8').strip() 57 except UnicodeDecodeError: 58 version = '' 59 ans = NO_CALIBRE_UPDATE 60 m = re.match(r'(\d+)\.(\d+).(\d+)$', version) 61 if m is not None: 62 ans = tuple(map(int, (m.group(1), m.group(2), m.group(3)))) 63 return ans 64 65 66class Signal(QObject): 67 68 update_found = pyqtSignal(object, object) 69 70 71class CheckForUpdates(Thread): 72 73 INTERVAL = 24*60*60 # seconds 74 daemon = True 75 76 def __init__(self, parent): 77 Thread.__init__(self) 78 self.shutdown_event = Event() 79 self.signal = Signal(parent) 80 81 def run(self): 82 while not self.shutdown_event.is_set(): 83 calibre_update_version = NO_CALIBRE_UPDATE 84 plugins_update_found = 0 85 try: 86 version = get_newest_version() 87 if version[:2] > numeric_version[:2]: 88 calibre_update_version = version 89 except Exception as e: 90 prints('Failed to check for calibre update:', as_unicode(e)) 91 try: 92 update_plugins = get_plugin_updates_available(raise_error=True) 93 if update_plugins is not None: 94 plugins_update_found = len(update_plugins) 95 except Exception as e: 96 prints('Failed to check for plugin update:', as_unicode(e)) 97 if calibre_update_version != NO_CALIBRE_UPDATE or plugins_update_found > 0: 98 self.signal.update_found.emit(calibre_update_version, plugins_update_found) 99 self.shutdown_event.wait(self.INTERVAL) 100 101 def shutdown(self): 102 self.shutdown_event.set() 103 104 105def version_key(calibre_version): 106 if isinstance(calibre_version, bytes): 107 calibre_version = calibre_version.decode('utf-8') 108 if calibre_version.count('.') > 1: 109 calibre_version = calibre_version.rpartition('.')[0] 110 return calibre_version 111 112 113def is_version_notified(calibre_version): 114 key = version_key(calibre_version) 115 done = dynamic.get('notified-version-updates') or set() 116 return key in done 117 118 119def save_version_notified(calibre_version): 120 done = dynamic.get('notified-version-updates') or set() 121 done.add(version_key(calibre_version)) 122 dynamic.set('notified-version-updates', done) 123 124 125class UpdateNotification(QDialog): 126 127 def __init__(self, calibre_version, plugin_updates, parent=None): 128 QDialog.__init__(self, parent) 129 self.setAttribute(Qt.WidgetAttribute.WA_QuitOnClose, False) 130 self.resize(400, 250) 131 self.l = QGridLayout() 132 self.setLayout(self.l) 133 self.logo = QLabel() 134 self.logo.setMaximumWidth(110) 135 self.logo.setPixmap(QIcon(I('lt.png')).pixmap(100, 100)) 136 ver = calibre_version 137 if ver.endswith('.0'): 138 ver = ver[:-2] 139 self.label = QLabel('<p>'+ _( 140 'New version <b>{ver}</b> of {app} is available for download. ' 141 'See the <a href="{url}">new features</a>.').format( 142 url=localize_website_link('https://calibre-ebook.com/whats-new'), 143 app=__appname__, ver=ver)) 144 self.label.setOpenExternalLinks(True) 145 self.label.setWordWrap(True) 146 self.setWindowTitle(_('Update available!')) 147 self.setWindowIcon(QIcon(I('lt.png'))) 148 self.l.addWidget(self.logo, 0, 0) 149 self.l.addWidget(self.label, 0, 1) 150 self.cb = QCheckBox( 151 _('Show this notification for future updates'), self) 152 self.l.addWidget(self.cb, 1, 0, 1, -1) 153 self.cb.setChecked(config.get('new_version_notification')) 154 self.cb.stateChanged.connect(self.show_future) 155 self.bb = QDialogButtonBox(self) 156 b = self.bb.addButton(_('&Get update'), QDialogButtonBox.ButtonRole.AcceptRole) 157 b.setDefault(True) 158 b.setIcon(QIcon(I('arrow-down.png'))) 159 if plugin_updates > 0: 160 b = self.bb.addButton(_('Update &plugins'), QDialogButtonBox.ButtonRole.ActionRole) 161 b.setIcon(QIcon(I('plugins/plugin_updater.png'))) 162 b.clicked.connect(self.get_plugins, type=Qt.ConnectionType.QueuedConnection) 163 self.bb.addButton(QDialogButtonBox.StandardButton.Cancel) 164 self.l.addWidget(self.bb, 2, 0, 1, -1) 165 self.bb.accepted.connect(self.accept) 166 self.bb.rejected.connect(self.reject) 167 save_version_notified(calibre_version) 168 169 def get_plugins(self): 170 from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog, 171 FILTER_UPDATE_AVAILABLE) 172 d = PluginUpdaterDialog(self.parent(), 173 initial_filter=FILTER_UPDATE_AVAILABLE) 174 d.exec() 175 if d.do_restart: 176 QDialog.accept(self) 177 from calibre.gui2.ui import get_gui 178 gui = get_gui() 179 if gui is not None: 180 gui.quit(restart=True) 181 182 def show_future(self, *args): 183 config.set('new_version_notification', bool(self.cb.isChecked())) 184 185 def accept(self): 186 open_url(QUrl(get_download_url())) 187 188 QDialog.accept(self) 189 190 191class UpdateMixin: 192 193 def __init__(self, *args, **kw): 194 pass 195 196 def init_update_mixin(self, opts): 197 self.last_newest_calibre_version = NO_CALIBRE_UPDATE 198 if not opts.no_update_check: 199 self.update_checker = CheckForUpdates(self) 200 self.update_checker.signal.update_found.connect(self.update_found, 201 type=Qt.ConnectionType.QueuedConnection) 202 self.update_checker.start() 203 204 def recalc_update_label(self, number_of_plugin_updates): 205 self.update_found(self.last_newest_calibre_version, number_of_plugin_updates) 206 207 def update_found(self, calibre_version, number_of_plugin_updates, force=False, no_show_popup=False): 208 self.last_newest_calibre_version = calibre_version 209 has_calibre_update = calibre_version != NO_CALIBRE_UPDATE and calibre_version[0] > 0 210 has_plugin_updates = number_of_plugin_updates > 0 211 self.plugin_update_found(number_of_plugin_updates) 212 version_url = as_hex_unicode(msgpack_dumps((calibre_version, number_of_plugin_updates))) 213 calibre_version = '.'.join(map(str, calibre_version)) 214 215 if not has_calibre_update and not has_plugin_updates: 216 self.status_bar.update_label.setVisible(False) 217 return 218 if has_calibre_update: 219 plt = '' 220 if has_plugin_updates: 221 plt = ngettext(' and one plugin update', ' and {} plugin updates', number_of_plugin_updates).format(number_of_plugin_updates) 222 msg = ('<span style="color:green; font-weight: bold">%s: ' 223 '<a href="update:%s">%s%s</a></span>') % ( 224 _('Update found'), version_url, calibre_version, plt) 225 else: 226 plt = ngettext('updated plugin', 'updated plugins', number_of_plugin_updates) 227 msg = ('<a href="update:%s">%d %s</a>')%(version_url, number_of_plugin_updates, plt) 228 self.status_bar.update_label.setText(msg) 229 self.status_bar.update_label.setVisible(True) 230 231 if has_calibre_update: 232 if (force or (config.get('new_version_notification') and not is_version_notified(calibre_version))): 233 if not no_show_popup: 234 self._update_notification__ = UpdateNotification(calibre_version, 235 number_of_plugin_updates, parent=self) 236 self._update_notification__.show() 237 elif has_plugin_updates: 238 if force: 239 from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog, 240 FILTER_UPDATE_AVAILABLE) 241 d = PluginUpdaterDialog(self, 242 initial_filter=FILTER_UPDATE_AVAILABLE) 243 d.exec() 244 if d.do_restart: 245 self.quit(restart=True) 246 247 def plugin_update_found(self, number_of_updates): 248 # Change the plugin icon to indicate there are updates available 249 plugin = self.iactions.get('Plugin Updater', None) 250 if not plugin: 251 return 252 if number_of_updates: 253 plugin.qaction.setText(_('Plugin updates')+'*') 254 plugin.qaction.setIcon(QIcon(I('plugins/plugin_updater_updates.png'))) 255 plugin.qaction.setToolTip( 256 ngettext('A plugin update is available', 257 'There are {} plugin updates available', number_of_updates).format(number_of_updates)) 258 else: 259 plugin.qaction.setText(_('Plugin updates')) 260 plugin.qaction.setIcon(QIcon(I('plugins/plugin_updater.png'))) 261 plugin.qaction.setToolTip(_('Install and configure user plugins')) 262 263 def update_link_clicked(self, url): 264 url = str(url) 265 if url.startswith('update:'): 266 calibre_version, number_of_plugin_updates = msgpack_loads(from_hex_bytes(url[len('update:'):])) 267 self.update_found(calibre_version, number_of_plugin_updates, force=True) 268 269 270if __name__ == '__main__': 271 from calibre.gui2 import Application 272 app = Application([]) 273 UpdateNotification('x.y.z', False).exec() 274 del app 275