1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net> 4 5 6from collections import defaultdict 7from threading import Thread 8 9from qt.core import ( 10 QCheckBox, QHBoxLayout, QIcon, QInputDialog, QLabel, QProgressBar, QSizePolicy, 11 QStackedWidget, Qt, QTextBrowser, QVBoxLayout, QWidget, pyqtSignal, QDialogButtonBox 12) 13 14from calibre.gui2 import error_dialog 15from calibre.gui2.tweak_book import current_container, editors, set_current_container, tprefs 16from calibre.gui2.tweak_book.boss import get_boss 17from calibre.gui2.tweak_book.widgets import Dialog 18from polyglot.builtins import iteritems 19 20 21def get_data(name): 22 'Get the data for name. Returns a unicode string if name is a text document/stylesheet' 23 if name in editors: 24 return editors[name].get_raw_data() 25 return current_container().raw_data(name) 26 27 28def set_data(name, val): 29 if name in editors: 30 editors[name].replace_data(val, only_if_different=False) 31 else: 32 with current_container().open(name, 'wb') as f: 33 if isinstance(val, str): 34 val = val.encode('utf-8') 35 f.write(val) 36 get_boss().set_modified() 37 38 39class CheckExternalLinks(Dialog): 40 41 progress_made = pyqtSignal(object, object) 42 43 def __init__(self, parent=None): 44 Dialog.__init__(self, _('Check external links'), 'check-external-links-dialog', parent) 45 self.progress_made.connect(self.on_progress_made, type=Qt.ConnectionType.QueuedConnection) 46 47 def show(self): 48 if self.rb.isEnabled(): 49 self.refresh() 50 return Dialog.show(self) 51 52 def refresh(self): 53 self.stack.setCurrentIndex(0) 54 self.rb.setEnabled(False) 55 t = Thread(name='CheckLinksMaster', target=self.run) 56 t.daemon = True 57 t.start() 58 59 def setup_ui(self): 60 self.pb = pb = QProgressBar(self) 61 pb.setTextVisible(True) 62 pb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) 63 pb.setRange(0, 0) 64 self.w = w = QWidget(self) 65 self.w.l = l = QVBoxLayout(w) 66 l.addStretch(), l.addWidget(pb) 67 self.w.la = la = QLabel(_('Checking external links, please wait...')) 68 la.setStyleSheet('QLabel { font-size: 20px; font-weight: bold }') 69 l.addWidget(la, 0, Qt.AlignmentFlag.AlignCenter), l.addStretch() 70 71 self.l = l = QVBoxLayout(self) 72 self.results = QTextBrowser(self) 73 self.results.setOpenLinks(False) 74 self.results.anchorClicked.connect(self.anchor_clicked) 75 self.stack = s = QStackedWidget(self) 76 s.addWidget(w), s.addWidget(self.results) 77 l.addWidget(s) 78 self.bh = h = QHBoxLayout() 79 self.check_anchors = ca = QCheckBox(_('Check &anchors')) 80 ca.setToolTip(_('Check HTML anchors in links (the part after the #).\n' 81 ' This can be a little slow, since it requires downloading and parsing all the HTML pages.')) 82 ca.setChecked(tprefs.get('check_external_link_anchors', True)) 83 ca.stateChanged.connect(self.anchors_changed) 84 h.addWidget(ca), h.addStretch(100), h.addWidget(self.bb) 85 l.addLayout(h) 86 self.bb.setStandardButtons(QDialogButtonBox.StandardButton.Close) 87 self.rb = b = self.bb.addButton(_('&Refresh'), QDialogButtonBox.ButtonRole.ActionRole) 88 b.setIcon(QIcon(I('view-refresh.png'))) 89 b.clicked.connect(self.refresh) 90 91 def anchors_changed(self): 92 tprefs.set('check_external_link_anchors', self.check_anchors.isChecked()) 93 94 def sizeHint(self): 95 ans = Dialog.sizeHint(self) 96 ans.setHeight(600) 97 ans.setWidth(max(ans.width(), 800)) 98 return ans 99 100 def run(self): 101 from calibre.ebooks.oeb.polish.check.links import check_external_links 102 self.tb = None 103 self.errors = [] 104 try: 105 self.errors = check_external_links(current_container(), self.progress_made.emit, check_anchors=self.check_anchors.isChecked()) 106 except Exception: 107 import traceback 108 self.tb = traceback.format_exc() 109 self.progress_made.emit(None, None) 110 111 def on_progress_made(self, curr, total): 112 if curr is None: 113 self.results.setText('') 114 self.stack.setCurrentIndex(1) 115 self.fixed_errors = set() 116 self.rb.setEnabled(True) 117 if self.tb is not None: 118 return error_dialog(self, _('Checking failed'), _( 119 'There was an error while checking links, click "Show details" for more information'), 120 det_msg=self.tb, show=True) 121 if not self.errors: 122 self.results.setText(_('No broken links found')) 123 else: 124 self.populate_results() 125 else: 126 self.pb.setMaximum(total), self.pb.setValue(curr) 127 128 def populate_results(self, preserve_pos=False): 129 num = len(self.errors) - len(self.fixed_errors) 130 text = '<h3>%s</h3><ol>' % (ngettext( 131 'Found a broken link', 'Found {} broken links', num).format(num)) 132 for i, (locations, err, url) in enumerate(self.errors): 133 if i in self.fixed_errors: 134 continue 135 text += '<li><b>%s</b> \xa0<a href="err:%d">[%s]</a><br>%s<br><ul>' % (url, i, _('Fix this link'), err) 136 for name, href, lnum, col in locations: 137 text += '<li>{name} \xa0<a href="loc:{lnum},{name}">[{line}: {lnum}]</a></li>'.format( 138 name=name, lnum=lnum, line=_('line number')) 139 text += '</ul></li><hr>' 140 self.results.setHtml(text) 141 142 def anchor_clicked(self, qurl): 143 url = qurl.toString() 144 if url.startswith('err:'): 145 errnum = int(url[4:]) 146 err = self.errors[errnum] 147 newurl, ok = QInputDialog.getText(self, _('Fix URL'), _('Enter the corrected URL:') + '\xa0'*40, text=err[2]) 148 if not ok: 149 return 150 nmap = defaultdict(set) 151 for name, href in {(l[0], l[1]) for l in err[0]}: 152 nmap[name].add(href) 153 154 for name, hrefs in iteritems(nmap): 155 raw = oraw = get_data(name) 156 for href in hrefs: 157 raw = raw.replace(href, newurl) 158 if raw != oraw: 159 set_data(name, raw) 160 self.fixed_errors.add(errnum) 161 self.populate_results() 162 elif url.startswith('loc:'): 163 lnum, name = url[4:].partition(',')[::2] 164 lnum = int(lnum or 1) 165 editor = get_boss().edit_file(name) 166 if lnum and editor is not None and editor.has_line_numbers: 167 editor.current_line = lnum 168 169 170if __name__ == '__main__': 171 import sys 172 from calibre.gui2 import Application 173 from calibre.gui2.tweak_book.boss import get_container 174 app = Application([]) 175 set_current_container(get_container(sys.argv[-1])) 176 d = CheckExternalLinks() 177 d.refresh() 178 d.exec() 179 del app 180