diff --git a/resources/recipes/ibm_smarter_planet.recipe b/resources/recipes/ibm_smarter_planet.recipe index 44978142f6..be26b29fc6 100644 --- a/resources/recipes/ibm_smarter_planet.recipe +++ b/resources/recipes/ibm_smarter_planet.recipe @@ -1,17 +1,18 @@ + from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1293122276(BasicNewsRecipe): - title = u'Smarter Planet | Tumblr for eReaders' + title = u'Smarter Planet | Tumblr' __author__ = 'Jack Mason' author = 'IBM Global Business Services' publisher = 'IBM' language = 'en' category = 'news, technology, IT, internet of things, analytics' - oldest_article = 7 + oldest_article = 14 max_articles_per_feed = 30 no_stylesheets = True use_embedded_content = False - masthead_url = 'http://30.media.tumblr.com/tumblr_l70dow9UmU1qzs4rbo1_r3_250.jpg' + masthead_url = 'http://www.hellercd.com/wp-content/uploads/2010/09/hero.jpg' remove_tags_before = dict(id='item') remove_tags_after = dict(id='item') remove_tags = [dict(attrs={'class':['sidebar', 'about', 'footer', 'description,' 'disqus', 'nav', 'notes', 'disqus_thread']}), @@ -21,4 +22,3 @@ class AdvancedUserRecipe1293122276(BasicNewsRecipe): feeds = [(u'Smarter Planet Tumblr', u'http://smarterplanet.tumblr.com/mobile/rss')] - diff --git a/src/calibre/devices/hanvon/driver.py b/src/calibre/devices/hanvon/driver.py index 1fe18afc58..f9dec178c6 100644 --- a/src/calibre/devices/hanvon/driver.py +++ b/src/calibre/devices/hanvon/driver.py @@ -24,7 +24,7 @@ class N516(USBMS): supported_platforms = ['windows', 'osx', 'linux'] # Ordered list of supported formats - FORMATS = ['epub', 'prc', 'html', 'pdf', 'txt'] + FORMATS = ['epub', 'prc', 'mobi', 'html', 'pdf', 'txt'] VENDOR_ID = [0x0525] PRODUCT_ID = [0xa4a5] diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index dd527ea0b5..5807ba5f8f 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -576,10 +576,12 @@ OptionRecommendation(name='sr3_replace', if not input_fmt: raise ValueError('Input file must have an extension') input_fmt = input_fmt[1:].lower() + self.archive_input_tdir = None if input_fmt in ('zip', 'rar', 'oebzip'): self.log('Processing archive...') - tdir = PersistentTemporaryDirectory('_plumber') + tdir = PersistentTemporaryDirectory('_plumber_archive') self.input, input_fmt = self.unarchive(self.input, tdir) + self.archive_input_tdir = tdir if os.access(self.input, os.R_OK): nfp = run_plugins_on_preprocess(self.input, input_fmt) if nfp != self.input: diff --git a/src/calibre/ebooks/metadata/covers.py b/src/calibre/ebooks/metadata/covers.py index 2f6fb46540..cbd8fc0e99 100644 --- a/src/calibre/ebooks/metadata/covers.py +++ b/src/calibre/ebooks/metadata/covers.py @@ -121,6 +121,7 @@ class LibraryThingCovers(CoverDownload): # {{{ LIBRARYTHING = 'http://www.librarything.com/isbn/' def get_cover_url(self, isbn, br, timeout=5.): + try: src = br.open_novisit('http://www.librarything.com/isbn/'+isbn, timeout=timeout).read().decode('utf-8', 'replace') @@ -129,6 +130,8 @@ class LibraryThingCovers(CoverDownload): # {{{ err = Exception(_('LibraryThing.com timed out. Try again later.')) raise err else: + if '/wiki/index.php/HelpThing:Verify' in src: + raise Exception('LibraryThing is blocking calibre.') s = BeautifulSoup(src) url = s.find('td', attrs={'class':'left'}) if url is None: @@ -142,9 +145,12 @@ class LibraryThingCovers(CoverDownload): # {{{ return url def has_cover(self, mi, ans, timeout=5.): - if not mi.isbn: + if not mi.isbn or not self.site_customization: return False - br = browser() + from calibre.ebooks.metadata.library_thing import get_browser, login + br = get_browser() + un, _, pw = self.site_customization.partition(':') + login(br, un, pw) try: self.get_cover_url(mi.isbn, br, timeout=timeout) self.debug('cover for', mi.isbn, 'found') @@ -153,9 +159,12 @@ class LibraryThingCovers(CoverDownload): # {{{ self.debug(e) def get_covers(self, mi, result_queue, abort, timeout=5.): - if not mi.isbn: + if not mi.isbn or not self.site_customization: return - br = browser() + from calibre.ebooks.metadata.library_thing import get_browser, login + br = get_browser() + un, _, pw = self.site_customization.partition(':') + login(br, un, pw) try: url = self.get_cover_url(mi.isbn, br, timeout=timeout) cover_data = br.open_novisit(url).read() @@ -164,6 +173,11 @@ class LibraryThingCovers(CoverDownload): # {{{ result_queue.put((False, self.exception_to_string(e), traceback.format_exc(), self.name)) + def customization_help(self, gui=False): + ans = _('To use librarything.com you must sign up for a %sfree account%s ' + 'and enter your username and password separated by a : below.') + return '

'+ans%('', '') + # }}} def check_for_cover(mi, timeout=5.): # {{{ diff --git a/src/calibre/ebooks/metadata/fetch.py b/src/calibre/ebooks/metadata/fetch.py index 8018f42b13..bd8d96a399 100644 --- a/src/calibre/ebooks/metadata/fetch.py +++ b/src/calibre/ebooks/metadata/fetch.py @@ -251,19 +251,26 @@ class LibraryThing(MetadataSource): # {{{ name = 'LibraryThing' metadata_type = 'social' - description = _('Downloads series/tags/rating information from librarything.com') + description = _('Downloads series/covers/rating information from librarything.com') def fetch(self): - if not self.isbn: + if not self.isbn or not self.site_customization: return from calibre.ebooks.metadata.library_thing import get_social_metadata + un, _, pw = self.site_customization.partition(':') try: self.results = get_social_metadata(self.title, self.book_author, - self.publisher, self.isbn) + self.publisher, self.isbn, username=un, password=pw) except Exception, e: self.exception = e self.tb = traceback.format_exc() + @property + def string_customization_help(self): + ans = _('To use librarything.com you must sign up for a %sfree account%s ' + 'and enter your username and password separated by a : below.') + return '

'+ans%('', '') + # }}} diff --git a/src/calibre/ebooks/metadata/library_thing.py b/src/calibre/ebooks/metadata/library_thing.py index d956747a2b..a0f28a3c21 100644 --- a/src/calibre/ebooks/metadata/library_thing.py +++ b/src/calibre/ebooks/metadata/library_thing.py @@ -4,14 +4,13 @@ __copyright__ = '2008, Kovid Goyal ' Fetch cover from LibraryThing.com based on ISBN number. ''' -import sys, socket, os, re, random +import sys, re, random from lxml import html import mechanize from calibre import browser, prints from calibre.utils.config import OptionParser -from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.chardet import strip_encoding_declarations OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false' @@ -28,6 +27,12 @@ def get_ua(): ] return choices[random.randint(0, len(choices)-1)] +_lt_br = None +def get_browser(): + global _lt_br + if _lt_br is None: + _lt_br = browser(user_agent=get_ua()) + return _lt_br.clone_browser() class HeadRequest(mechanize.Request): @@ -35,7 +40,7 @@ class HeadRequest(mechanize.Request): return 'HEAD' def check_for_cover(isbn, timeout=5.): - br = browser(user_agent=get_ua()) + br = get_browser() br.set_handle_redirect(False) try: br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout) @@ -54,46 +59,16 @@ class ISBNNotFound(LibraryThingError): class ServerBusy(LibraryThingError): pass -def login(br, username, password, force=True): - br.open('http://www.librarything.com') +def login(br, username, password): + raw = br.open('http://www.librarything.com').read() + if '>Sign out' in raw: + return br.select_form('signup') br['formusername'] = username br['formpassword'] = password - br.submit() - - -def cover_from_isbn(isbn, timeout=5., username=None, password=None): - src = None - br = browser(user_agent=get_ua()) - try: - return br.open(OPENLIBRARY%isbn, timeout=timeout).read(), 'jpg' - except: - pass # Cover not found - if username and password: - try: - login(br, username, password, force=False) - except: - pass - try: - src = br.open_novisit('http://www.librarything.com/isbn/'+isbn, - timeout=timeout).read().decode('utf-8', 'replace') - except Exception, err: - if isinstance(getattr(err, 'args', [None])[0], socket.timeout): - err = LibraryThingError(_('LibraryThing.com timed out. Try again later.')) - raise err - else: - s = BeautifulSoup(src) - url = s.find('td', attrs={'class':'left'}) - if url is None: - if s.find('div', attrs={'class':'highloadwarning'}) is not None: - raise ServerBusy(_('Could not fetch cover as server is experiencing high load. Please try again later.')) - raise ISBNNotFound('ISBN: '+isbn+_(' not found.')) - url = url.find('img') - if url is None: - raise LibraryThingError(_('LibraryThing.com server error. Try again later.')) - url = re.sub(r'_S[XY]\d+', '', url['src']) - cover_data = br.open_novisit(url).read() - return cover_data, url.rpartition('.')[-1] + raw = br.submit().read() + if '>Sign out' not in raw: + raise ValueError('Failed to login as %r:%r'%(username, password)) def option_parser(): parser = OptionParser(usage=\ @@ -113,15 +88,16 @@ def get_social_metadata(title, authors, publisher, isbn, username=None, from calibre.ebooks.metadata import MetaInformation mi = MetaInformation(title, authors) if isbn: - br = browser(user_agent=get_ua()) - if username and password: - try: - login(br, username, password, force=False) - except: - pass + br = get_browser() + try: + login(br, username, password) - raw = br.open_novisit('http://www.librarything.com/isbn/' - +isbn).read() + raw = br.open_novisit('http://www.librarything.com/isbn/' + +isbn).read() + except: + return mi + if '/wiki/index.php/HelpThing:Verify' in raw: + raise Exception('LibraryThing is blocking calibre.') if not raw: return mi raw = raw.decode('utf-8', 'replace') @@ -172,15 +148,46 @@ def main(args=sys.argv): parser.print_help() return 1 isbn = args[1] - mi = get_social_metadata('', [], '', isbn) + from calibre.customize.ui import metadata_sources, cover_sources + lt = None + for x in metadata_sources('social'): + if x.name == 'LibraryThing': + lt = x + break + lt('', '', '', isbn, True) + lt.join() + if lt.exception: + print lt.tb + return 1 + mi = lt.results prints(mi) - cover_data, ext = cover_from_isbn(isbn, username=opts.username, - password=opts.password) - if not ext: - ext = 'jpg' - oname = os.path.abspath(isbn+'.'+ext) - open(oname, 'w').write(cover_data) - print 'Cover saved to file', oname + mi.isbn = isbn + + lt = None + for x in cover_sources(): + if x.name == 'librarything.com covers': + lt = x + break + + from threading import Event + from Queue import Queue + ev = Event() + lt.has_cover(mi, ev) + hc = ev.is_set() + print 'Has cover:', hc + if hc: + abort = Event() + temp = Queue() + lt.get_covers(mi, temp, abort) + + cover = temp.get_nowait() + if cover[0]: + open(isbn + '.jpg', 'wb').write(cover[1]) + print 'Cover saved to:', isbn+'.jpg' + else: + print 'Cover download failed' + print cover[2] + return 0 if __name__ == '__main__': diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 84a26cea18..a8f80ab35a 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -8,12 +8,12 @@ from urllib import unquote from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ QByteArray, QTranslator, QCoreApplication, QThread, \ QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ - QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ - QIcon, QApplication, QDialog, QPushButton, QUrl, QFont + QFileDialog, QFileIconProvider, \ + QIcon, QApplication, QDialog, QUrl, QFont ORG_NAME = 'KovidsBrain' APP_UID = 'libprs500' -from calibre.constants import islinux, iswindows, isosx, isfreebsd, isfrozen +from calibre.constants import islinux, iswindows, isfreebsd, isfrozen from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig from calibre.utils.localization import set_qt_translator from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats @@ -178,104 +178,40 @@ def is_widescreen(): def extension(path): return os.path.splitext(path)[1][1:].lower() -class CopyButton(QPushButton): - - ACTION_KEYS = [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Space] - - def copied(self): - self.emit(SIGNAL('copy()')) - self.setDisabled(True) - self.setText(_('Copied')) - - - def keyPressEvent(self, ev): - try: - if ev.key() in self.ACTION_KEYS: - self.copied() - return - except: - pass - QPushButton.keyPressEvent(self, ev) - - - def keyReleaseEvent(self, ev): - try: - if ev.key() in self.ACTION_KEYS: - return - except: - pass - QPushButton.keyReleaseEvent(self, ev) - - def mouseReleaseEvent(self, ev): - ev.accept() - self.copied() - -class MessageBox(QMessageBox): - - def __init__(self, type_, title, msg, buttons, parent, det_msg=''): - QMessageBox.__init__(self, type_, title, msg, buttons, parent) - self.title = title - self.msg = msg - self.det_msg = det_msg - self.setDetailedText(det_msg) - # Cannot set keyboard shortcut as the event is not easy to filter - self.cb = CopyButton(_('Copy') if isosx else _('Copy to Clipboard')) - self.connect(self.cb, SIGNAL('copy()'), self.copy_to_clipboard) - self.addButton(self.cb, QMessageBox.ActionRole) - default_button = self.button(self.Ok) - if default_button is None: - default_button = self.button(self.Yes) - if default_button is not None: - self.setDefaultButton(default_button) - - def copy_to_clipboard(self): - QApplication.clipboard().setText('%s: %s\n\n%s' % - (self.title, self.msg, self.det_msg)) - - def warning_dialog(parent, title, msg, det_msg='', show=False, show_copy_button=True): - d = MessageBox(QMessageBox.Warning, 'WARNING: '+title, msg, QMessageBox.Ok, - parent, det_msg) - d.setEscapeButton(QMessageBox.Ok) - d.setIconPixmap(QPixmap(I('dialog_warning.png'))) - if not show_copy_button: - d.cb.setVisible(False) + from calibre.gui2.dialogs.message_box import MessageBox + d = MessageBox(MessageBox.WARNING, 'WARNING: '+title, msg, det_msg, parent=parent, + show_copy_button=show_copy_button) if show: return d.exec_() return d def error_dialog(parent, title, msg, det_msg='', show=False, show_copy_button=True): - d = MessageBox(QMessageBox.Critical, 'ERROR: '+title, msg, QMessageBox.Ok, - parent, det_msg) - d.setIconPixmap(QPixmap(I('dialog_error.png'))) - d.setEscapeButton(QMessageBox.Ok) - if not show_copy_button: - d.cb.setVisible(False) + from calibre.gui2.dialogs.message_box import MessageBox + d = MessageBox(MessageBox.ERROR, 'ERROR: '+title, msg, det_msg, parent=parent, + show_copy_button=show_copy_button) if show: return d.exec_() return d -def question_dialog(parent, title, msg, det_msg='', show_copy_button=True, - buttons=QMessageBox.Yes|QMessageBox.No, yes_button=QMessageBox.Yes): - d = MessageBox(QMessageBox.Question, title, msg, buttons, - parent, det_msg) - d.setIconPixmap(QPixmap(I('dialog_question.png'))) - d.setEscapeButton(QMessageBox.No) - if not show_copy_button: - d.cb.setVisible(False) +def question_dialog(parent, title, msg, det_msg='', show_copy_button=False, + buttons=None, yes_button=None): + from calibre.gui2.dialogs.message_box import MessageBox + d = MessageBox(MessageBox.QUESTION, title, msg, det_msg, parent=parent, + show_copy_button=show_copy_button) + if buttons is not None: + d.bb.setStandardButtons(buttons) - return d.exec_() == yes_button + return d.exec_() == d.Accepted def info_dialog(parent, title, msg, det_msg='', show=False, show_copy_button=True): - d = MessageBox(QMessageBox.Information, title, msg, QMessageBox.Ok, - parent, det_msg) - d.setIconPixmap(QPixmap(I('dialog_information.png'))) - if not show_copy_button: - d.cb.setVisible(False) + from calibre.gui2.dialogs.message_box import MessageBox + d = MessageBox(MessageBox.INFO, title, msg, det_msg, parent=parent, + show_copy_button=show_copy_button) if show: return d.exec_() diff --git a/src/calibre/gui2/convert/bulk.py b/src/calibre/gui2/convert/bulk.py index 591ac92b2b..349f39ac76 100644 --- a/src/calibre/gui2/convert/bulk.py +++ b/src/calibre/gui2/convert/bulk.py @@ -4,6 +4,8 @@ __license__ = 'GPL 3' __copyright__ = '2009, John Schember ' __docformat__ = 'restructuredtext en' +import shutil + from PyQt4.Qt import QString, SIGNAL from calibre.gui2.convert.single import Config, sort_formats_by_preference, \ @@ -108,6 +110,11 @@ class BulkConfig(Config): idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0 self.groups.setCurrentIndex(self._groups_model.index(idx)) self.stack.setCurrentIndex(idx) + try: + shutil.rmtree(self.plumber.archive_input_tdir, ignore_errors=True) + except: + pass + def setup_output_formats(self, db, preferred_output_format): if preferred_output_format: diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py index da58de545b..59fcbb65ad 100644 --- a/src/calibre/gui2/convert/single.py +++ b/src/calibre/gui2/convert/single.py @@ -6,7 +6,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import sys, cPickle +import sys, cPickle, shutil from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont @@ -224,6 +224,10 @@ class Config(ResizableDialog, Ui_Dialog): idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0 self.groups.setCurrentIndex(self._groups_model.index(idx)) self.stack.setCurrentIndex(idx) + try: + shutil.rmtree(self.plumber.archive_input_tdir, ignore_errors=True) + except: + pass def setup_input_output_formats(self, db, book_id, preferred_input_format, diff --git a/src/calibre/gui2/dialogs/message_box.py b/src/calibre/gui2/dialogs/message_box.py new file mode 100644 index 0000000000..123476b734 --- /dev/null +++ b/src/calibre/gui2/dialogs/message_box.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from PyQt4.Qt import QDialog, QIcon, QApplication, QSize, QKeySequence, \ + QAction, Qt + +from calibre.constants import __version__ +from calibre.gui2.dialogs.message_box_ui import Ui_Dialog + +class MessageBox(QDialog, Ui_Dialog): + + ERROR = 0 + WARNING = 1 + INFO = 2 + QUESTION = 3 + + def __init__(self, type_, title, msg, det_msg='', show_copy_button=True, + parent=None): + QDialog.__init__(self, parent) + icon = { + self.ERROR : 'error', + self.WARNING: 'warning', + self.INFO: 'information', + self.QUESTION: 'question', + }[type_] + icon = 'dialog_%s.png'%icon + self.icon = QIcon(I(icon)) + self.setupUi(self) + + self.setWindowTitle(title) + self.setWindowIcon(self.icon) + self.icon_label.setPixmap(self.icon.pixmap(128, 128)) + self.msg.setText(msg) + self.det_msg.setPlainText(det_msg) + self.det_msg.setVisible(False) + + if det_msg: + self.show_det_msg = _('Show &details') + self.hide_det_msg = _('Hide &details') + self.det_msg_toggle = self.bb.addButton(self.show_det_msg, self.bb.ActionRole) + self.det_msg_toggle.clicked.connect(self.toggle_det_msg) + self.det_msg_toggle.setToolTip( + _('Show detailed information about this error')) + + if show_copy_button: + self.ctc_button = self.bb.addButton(_('&Copy to clipboard'), + self.bb.ActionRole) + self.ctc_button.clicked.connect(self.copy_to_clipboard) + + + self.copy_action = QAction(self) + self.addAction(self.copy_action) + self.copy_action.setShortcuts(QKeySequence.Copy) + self.copy_action.triggered.connect(self.copy_to_clipboard) + + self.is_question = type_ == self.QUESTION + if self.is_question: + self.bb.setStandardButtons(self.bb.Yes|self.bb.No) + self.bb.button(self.bb.Yes).setDefault(True) + else: + self.bb.button(self.bb.Ok).setDefault(True) + + self.do_resize() + + def toggle_det_msg(self, *args): + vis = self.det_msg.isVisible() + self.det_msg_toggle.setText(self.show_det_msg if vis else + self.hide_det_msg) + self.det_msg.setVisible(not vis) + self.do_resize() + + def do_resize(self): + sz = self.sizeHint() + QSize(100, 0) + sz.setWidth(min(500, sz.width())) + sz.setHeight(min(500, sz.height())) + self.resize(sz) + + def copy_to_clipboard(self, *args): + QApplication.clipboard().setText( + 'calibre, version %s\n%s: %s\n\n%s' % + (__version__, unicode(self.windowTitle()), + unicode(self.msg.text()), + unicode(self.det_msg.toPlainText()))) + self.ctc_button.setText(_('Copied')) + + def showEvent(self, ev): + ret = QDialog.showEvent(self, ev) + if self.is_question: + self.bb.button(self.bb.Yes).setFocus(Qt.OtherFocusReason) + else: + self.bb.button(self.bb.Ok).setFocus(Qt.OtherFocusReason) + return ret + +if __name__ == '__main__': + app = QApplication([]) + from calibre.gui2 import question_dialog + print question_dialog(None, 'title', 'msg goog ', + det_msg='det '*1000, + show_copy_button=True) diff --git a/src/calibre/gui2/dialogs/message_box.ui b/src/calibre/gui2/dialogs/message_box.ui new file mode 100644 index 0000000000..136e6d250e --- /dev/null +++ b/src/calibre/gui2/dialogs/message_box.ui @@ -0,0 +1,105 @@ + + + Dialog + + + + 0 + 0 + 497 + 235 + + + + Dialog + + + + + + + 68 + 68 + + + + + + + :/images/dialog_warning.png + + + true + + + + + + + + + + true + + + true + + + + + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + + + + bb + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + bb + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +