Sync to trunk.

This commit is contained in:
John Schember 2011-06-13 18:22:47 -04:00
commit ed941e3f8c
19 changed files with 115 additions and 41 deletions

View File

@ -6,7 +6,7 @@ class HBR(BasicNewsRecipe):
title = 'Harvard Business Review Blogs' title = 'Harvard Business Review Blogs'
description = 'To subscribe go to http://hbr.harvardbusiness.org' description = 'To subscribe go to http://hbr.harvardbusiness.org'
needs_subscription = True needs_subscription = True
__author__ = 'Kovid Goyal and Sujata Raman, enhanced by BrianG' __author__ = 'Kovid Goyal, enhanced by BrianG'
language = 'en' language = 'en'
no_stylesheets = True no_stylesheets = True

View File

@ -603,10 +603,11 @@ from calibre.devices.eslick.driver import ESLICK, EBK52
from calibre.devices.nuut2.driver import NUUT2 from calibre.devices.nuut2.driver import NUUT2
from calibre.devices.iriver.driver import IRIVER_STORY from calibre.devices.iriver.driver import IRIVER_STORY
from calibre.devices.binatone.driver import README from calibre.devices.binatone.driver import README
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK from calibre.devices.hanvon.driver import (N516, EB511, ALEX, AZBOOKA, THEBOOK,
LIBREAIR)
from calibre.devices.edge.driver import EDGE from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \ from calibre.devices.teclast.driver import (TECLAST_K3, NEWSMY, IPAPYRUS,
SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER)
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
from calibre.devices.misc import (PALMPRE, AVANT, SWEEX, PDNOVEL, from calibre.devices.misc import (PALMPRE, AVANT, SWEEX, PDNOVEL,
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, LUMIREAD, ALURATEK_COLOR, GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, LUMIREAD, ALURATEK_COLOR,
@ -716,7 +717,7 @@ plugins += [
EB600, EB600,
README, README,
N516, N516,
THEBOOK, THEBOOK, LIBREAIR,
EB511, EB511,
ELONEX, ELONEX,
TECLAST_K3, TECLAST_K3,

View File

@ -52,6 +52,18 @@ class THEBOOK(N516):
EBOOK_DIR_MAIN = 'My books' EBOOK_DIR_MAIN = 'My books'
WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE' WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE'
class LIBREAIR(N516):
name = 'Libre Air Driver'
gui_name = 'Libre Air'
description = _('Communicate with the Libre Air reader.')
author = 'Kovid Goyal'
FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'rtf', 'txt', 'pdf']
BCD = [0x399]
VENDOR_NAME = 'ALURATEK'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGET'
EBOOK_DIR_MAIN = 'Books'
class ALEX(N516): class ALEX(N516):
name = 'Alex driver' name = 'Alex driver'

View File

@ -394,6 +394,13 @@ class EPUBOutput(OutputFormatPlugin):
for tag in XPath('//h:img[@src]')(root): for tag in XPath('//h:img[@src]')(root):
tag.set('src', tag.get('src', '').replace('&', '')) tag.set('src', tag.get('src', '').replace('&', ''))
# ADE whimpers in fright when it encounters a <td> outside a
# <table>
in_table = XPath('ancestor::h:table')
for tag in XPath('//h:td|//h:tr|//h:th')(root):
if not in_table(tag):
tag.tag = XHTML('div')
special_chars = re.compile(u'[\u200b\u00ad]') special_chars = re.compile(u'[\u200b\u00ad]')
for elem in root.iterdescendants(): for elem in root.iterdescendants():
if getattr(elem, 'text', False): if getattr(elem, 'text', False):
@ -413,7 +420,7 @@ class EPUBOutput(OutputFormatPlugin):
rule.style.removeProperty('margin-left') rule.style.removeProperty('margin-left')
# padding-left breaks rendering in webkit and gecko # padding-left breaks rendering in webkit and gecko
rule.style.removeProperty('padding-left') rule.style.removeProperty('padding-left')
# Change whitespace:pre to pre-line to accommodate readers that # Change whitespace:pre to pre-wrap to accommodate readers that
# cannot scroll horizontally # cannot scroll horizontally
for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE): for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
style = rule.style style = rule.style

View File

@ -442,9 +442,12 @@ class MobiMLizer(object):
if tag in TABLE_TAGS and self.ignore_tables: if tag in TABLE_TAGS and self.ignore_tables:
tag = 'span' if tag == 'td' else 'div' tag = 'span' if tag == 'td' else 'div'
# GR: Added 'width', 'border' and 'scope' if tag == 'table':
css = style.cssdict()
if 'border' in css or 'border-width' in css:
elem.set('border', '1')
if tag in TABLE_TAGS: if tag in TABLE_TAGS:
for attr in ('rowspan', 'colspan','width','border','scope'): for attr in ('rowspan', 'colspan', 'width', 'border', 'scope'):
if attr in elem.attrib: if attr in elem.attrib:
istate.attrib[attr] = elem.attrib[attr] istate.attrib[attr] = elem.attrib[attr]
if tag == 'q': if tag == 'q':

View File

@ -348,7 +348,6 @@ class MobiReader(object):
self.processed_html = self.remove_random_bytes(self.processed_html) self.processed_html = self.remove_random_bytes(self.processed_html)
root = soupparser.fromstring(self.processed_html) root = soupparser.fromstring(self.processed_html)
if root.tag != 'html': if root.tag != 'html':
self.log.warn('File does not have opening <html> tag') self.log.warn('File does not have opening <html> tag')
nroot = html.fromstring('<html><head></head><body></body></html>') nroot = html.fromstring('<html><head></head><body></body></html>')

View File

@ -597,7 +597,7 @@ class DeviceMenu(QMenu): # {{{
# }}} # }}}
class DeviceSignals(QObject): class DeviceSignals(QObject): # {{{
#: This signal is emitted once, after metadata is downloaded from the #: This signal is emitted once, after metadata is downloaded from the
#: connected device. #: connected device.
#: The sequence: gui.device_manager.is_device_connected will become True, #: The sequence: gui.device_manager.is_device_connected will become True,
@ -614,6 +614,7 @@ class DeviceSignals(QObject):
device_connection_changed = pyqtSignal(object) device_connection_changed = pyqtSignal(object)
device_signals = DeviceSignals() device_signals = DeviceSignals()
# }}}
class DeviceMixin(object): # {{{ class DeviceMixin(object): # {{{

View File

@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en'
import os, socket, time import os, socket, time
from binascii import unhexlify from binascii import unhexlify
from functools import partial from functools import partial
from threading import Thread
from itertools import repeat from itertools import repeat
from calibre.utils.smtp import (compose_mail, sendmail, extract_email_address, from calibre.utils.smtp import (compose_mail, sendmail, extract_email_address,
@ -22,9 +23,30 @@ from calibre.library.save_to_disk import get_components
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.gui2.threaded_jobs import ThreadedJob from calibre.gui2.threaded_jobs import ThreadedJob
class Worker(Thread):
def __init__(self, func, args):
Thread.__init__(self)
self.daemon = True
self.exception = self.tb = None
self.func, self.args = func, args
def run(self):
#time.sleep(1000)
try:
self.func(*self.args)
except Exception as e:
import traceback
self.exception = e
self.tb = traceback.format_exc()
finally:
self.func = self.args = None
class Sendmail(object): class Sendmail(object):
MAX_RETRIES = 1 MAX_RETRIES = 1
TIMEOUT = 15 * 60 # seconds
def __init__(self): def __init__(self):
self.calculate_rate_limit() self.calculate_rate_limit()
@ -42,22 +64,32 @@ class Sendmail(object):
abort=None, notifications=None): abort=None, notifications=None):
try_count = 0 try_count = 0
while try_count <= self.MAX_RETRIES: while True:
if try_count > 0: if try_count > 0:
log('\nRetrying in %d seconds...\n' % log('\nRetrying in %d seconds...\n' %
self.rate_limit) self.rate_limit)
try: worker = Worker(self.sendmail,
self.sendmail(attachment, aname, to, subject, text, log) (attachment, aname, to, subject, text, log))
try_count = self.MAX_RETRIES worker.start()
log('Email successfully sent') start_time = time.time()
except: while worker.is_alive():
worker.join(0.2)
if abort.is_set(): if abort.is_set():
log('Sending aborted by user')
return return
if try_count == self.MAX_RETRIES: if time.time() - start_time > self.TIMEOUT:
raise log('Sending timed out')
log.exception('\nSending failed...\n') raise Exception(
'Sending email %r to %r timed out, aborting'% (subject,
to))
if worker.exception is None:
log('Email successfully sent')
return
log.error('\nSending failed...\n')
log.debug(worker.tb)
try_count += 1 try_count += 1
if try_count > self.MAX_RETRIES:
raise worker.exception
def sendmail(self, attachment, aname, to, subject, text, log): def sendmail(self, attachment, aname, to, subject, text, log):
while time.time() - self.last_send_time <= self.rate_limit: while time.time() - self.last_send_time <= self.rate_limit:
@ -90,7 +122,7 @@ def send_mails(jobnames, callback, attachments, to_s, subjects,
attachments, to_s, subjects, texts, attachment_names): attachments, to_s, subjects, texts, attachment_names):
description = _('Email %s to %s') % (name, to) description = _('Email %s to %s') % (name, to)
job = ThreadedJob('email', description, gui_sendmail, (attachment, aname, to, job = ThreadedJob('email', description, gui_sendmail, (attachment, aname, to,
subject, text), {}, callback, killable=False) subject, text), {}, callback)
job_manager.run_threaded_job(job) job_manager.run_threaded_job(job)

View File

@ -457,7 +457,7 @@ class JobsDialog(QDialog, Ui_JobsDialog):
def kill_job(self, *args): def kill_job(self, *args):
if question_dialog(self, _('Are you sure?'), _('Do you really want to stop the selected job?')): if question_dialog(self, _('Are you sure?'), _('Do you really want to stop the selected job?')):
for index in self.jobs_view.selectedIndexes(): for index in self.jobs_view.selectionModel().selectedRows():
row = index.row() row = index.row()
self.model.kill_job(row, self) self.model.kill_job(row, self)

View File

@ -1110,6 +1110,8 @@ class DeviceBooksModel(BooksModel): # {{{
if self.last_search: if self.last_search:
self.searched.emit(True) self.searched.emit(True)
def research(self, reset=True):
self.search(self.last_search, reset)
def sort(self, col, order, reset=True): def sort(self, col, order, reset=True):
descending = order != Qt.AscendingOrder descending = order != Qt.AscendingOrder
@ -1171,6 +1173,8 @@ class DeviceBooksModel(BooksModel): # {{{
self.custom_columns = {} self.custom_columns = {}
self.db = db self.db = db
self.map = list(range(0, len(db))) self.map = list(range(0, len(db)))
self.research(reset=False)
self.resort()
def cover(self, row): def cover(self, row):
item = self.db[self.map[row]] item = self.db[self.map[row]]
@ -1319,8 +1323,6 @@ class DeviceBooksModel(BooksModel): # {{{
ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname, ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,
'left')] 'left')]
return QVariant(ans) return QVariant(ans)
return NONE return NONE
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):

View File

@ -48,7 +48,7 @@ class BooksView(QTableView): # {{{
files_dropped = pyqtSignal(object) files_dropped = pyqtSignal(object)
add_column_signal = pyqtSignal() add_column_signal = pyqtSignal()
def __init__(self, parent, modelcls=BooksModel): def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True):
QTableView.__init__(self, parent) QTableView.__init__(self, parent)
self.setEditTriggers(self.EditKeyPressed) self.setEditTriggers(self.EditKeyPressed)
@ -60,8 +60,12 @@ class BooksView(QTableView): # {{{
elif tweaks['doubleclick_on_library_view'] == 'edit_metadata': elif tweaks['doubleclick_on_library_view'] == 'edit_metadata':
# Must not enable single-click to edit, or the field will remain # Must not enable single-click to edit, or the field will remain
# open in edit mode underneath the edit metadata dialog # open in edit mode underneath the edit metadata dialog
self.doubleClicked.connect( if use_edit_metadata_dialog:
partial(parent.iactions['Edit Metadata'].edit_metadata, checked=False)) self.doubleClicked.connect(
partial(parent.iactions['Edit Metadata'].edit_metadata,
checked=False))
else:
self.setEditTriggers(self.DoubleClicked|self.editTriggers())
self.drag_allowed = True self.drag_allowed = True
self.setDragEnabled(True) self.setDragEnabled(True)
@ -792,7 +796,8 @@ class BooksView(QTableView): # {{{
class DeviceBooksView(BooksView): # {{{ class DeviceBooksView(BooksView): # {{{
def __init__(self, parent): def __init__(self, parent):
BooksView.__init__(self, parent, DeviceBooksModel) BooksView.__init__(self, parent, DeviceBooksModel,
use_edit_metadata_dialog=False)
self.can_add_columns = False self.can_add_columns = False
self.columns_resized = False self.columns_resized = False
self.resize_on_select = False self.resize_on_select = False

View File

@ -462,11 +462,11 @@ class EditRules(QWidget): # {{{
self.l = l = QGridLayout(self) self.l = l = QGridLayout(self)
self.setLayout(l) self.setLayout(l)
self.l1 = l1 = QLabel(_( self.l1 = l1 = QLabel('<p>'+_(
'You can control the color of columns in the' 'You can control the color of columns in the'
' book list by creating "rules" that tell calibre' ' book list by creating "rules" that tell calibre'
' what color to use. Click the Add Rule button below' ' what color to use. Click the Add Rule button below'
' to get started. You can change an existing rule by double' ' to get started.<p>You can <b>change an existing rule</b> by double'
' clicking it.')) ' clicking it.'))
l1.setWordWrap(True) l1.setWordWrap(True)
l.addWidget(l1, 0, 0, 1, 2) l.addWidget(l1, 0, 0, 1, 2)

View File

@ -610,7 +610,7 @@ class TagTreeItem(object): # {{{
self.temporary = temporary self.temporary = temporary
self.tag = Tag(data, category=category_key, self.tag = Tag(data, category=category_key,
is_editable=category_key not in ['news', 'search', 'identifiers'], is_editable=category_key not in ['news', 'search', 'identifiers'],
is_searchable=category_key not in ['news', 'search']) is_searchable=category_key not in ['search'])
elif self.type == self.TAG: elif self.type == self.TAG:
self.icon_state_map[0] = QVariant(data.icon) self.icon_state_map[0] = QVariant(data.icon)
@ -1642,7 +1642,13 @@ class TagsModel(QAbstractItemModel): # {{{
for node in self.category_nodes: for node in self.category_nodes:
if node.tag.state: if node.tag.state:
ans.append('%s:%s'%(node.category_key, node_searches[node.tag.state])) if node.category_key == "news":
if node_searches[node.tag.state] == 'true':
ans.append('tags:=news')
else:
ans.append('( not tags:=news )')
else:
ans.append('%s:%s'%(node.category_key, node_searches[node.tag.state]))
key = node.category_key key = node.category_key
for tag_item in node.child_tags(): for tag_item in node.child_tags():

View File

@ -26,7 +26,7 @@ from calibre.library.custom_columns import CustomColumns
from calibre.library.sqlite import connect, IntegrityError from calibre.library.sqlite import connect, IntegrityError
from calibre.library.prefs import DBPrefs from calibre.library.prefs import DBPrefs
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding from calibre.constants import preferred_encoding, iswindows, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
from calibre import isbytestring from calibre import isbytestring
@ -188,8 +188,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
apply_default_prefs = not os.path.exists(self.dbpath) apply_default_prefs = not os.path.exists(self.dbpath)
self.connect() self.connect()
self.is_case_sensitive = not iswindows and not isosx and \ self.is_case_sensitive = (not iswindows and
not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) not os.path.exists(self.dbpath.replace('metadata.db',
'MeTAdAtA.dB')))
SchemaUpgrade.__init__(self) SchemaUpgrade.__init__(self)
# Guarantee that the library_id is set # Guarantee that the library_id is set
self.library_id self.library_id
@ -606,7 +607,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
f = self.format(id, format, index_is_id=True, as_file=True) f = self.format(id, format, index_is_id=True, as_file=True)
if f is None: if f is None:
continue continue
with tempfile.SpooledTemporaryFile(max_size=100*(1024**2)) as stream: with tempfile.SpooledTemporaryFile(max_size=30*(1024**2)) as stream:
with f: with f:
shutil.copyfileobj(f, stream) shutil.copyfileobj(f, stream)
stream.seek(0) stream.seek(0)

View File

@ -189,7 +189,7 @@ Extra CSS
This option allows you to specify arbitrary CSS that will be applied to all HTML files in the This option allows you to specify arbitrary CSS that will be applied to all HTML files in the
input. This CSS is applied with very high priority and so should override most CSS present in input. This CSS is applied with very high priority and so should override most CSS present in
the input document itself. You can use this setting to fine tune the presentation/layout of your the **input document** itself. You can use this setting to fine tune the presentation/layout of your
document. For example, if you want all paragraphs of class `endnote` to be right aligned, just document. For example, if you want all paragraphs of class `endnote` to be right aligned, just
add:: add::
@ -200,7 +200,8 @@ or if you want to change the indentation of all paragraphs::
p { text-indent: 5mm; } p { text-indent: 5mm; }
:guilabel:`Extra CSS` is a very powerful option, but you do need an understanding of how CSS works :guilabel:`Extra CSS` is a very powerful option, but you do need an understanding of how CSS works
to use it to its full potential. to use it to its full potential. You can use the debug pipeline option described above to see what
CSS is present in your input document.
Miscellaneous Miscellaneous
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@ -417,7 +417,7 @@ You might find the following tips useful.
* Create a custom composite column to test templates. Once you have the column, you can change its template simply by double-clicking on the column. Hide the column when you are not testing. * Create a custom composite column to test templates. Once you have the column, you can change its template simply by double-clicking on the column. Hide the column when you are not testing.
* Templates can use other templates by referencing a composite custom column. * Templates can use other templates by referencing a composite custom column.
* In a plugboard, you can set a field to empty (or whatever is equivalent to empty) by using the special template ``{null}``. This template will always evaluate to an empty string. * In a plugboard, you can set a field to empty (or whatever is equivalent to empty) by using the special template ``{}``. This template will always evaluate to an empty string.
* The technique described above to show numbers even if they have a zero value works with the standard field series_index. * The technique described above to show numbers even if they have a zero value works with the standard field series_index.
.. toctree:: .. toctree::

View File

@ -317,6 +317,9 @@ def feed_from_xml(raw_xml, title=None, oldest_article=7,
max_articles_per_feed=100, max_articles_per_feed=100,
get_article_url=lambda item: item.get('link', None), get_article_url=lambda item: item.get('link', None),
log=default_log): log=default_log):
# Handle unclosed escaped entities. They trip up feedparser and HBR for one
# generates them
raw_xml = re.sub(r'(&amp;#\d+)([^0-9;])', r'\1;\2', raw_xml)
feed = parse(raw_xml) feed = parse(raw_xml)
pfeed = Feed(get_article_url=get_article_url, log=log) pfeed = Feed(get_article_url=get_article_url, log=log)
pfeed.populate_from_feed(feed, title=title, pfeed.populate_from_feed(feed, title=title,

View File

@ -13,8 +13,8 @@ from functools import partial
from contextlib import nested, closing from contextlib import nested, closing
from calibre import browser, __appname__, iswindows, \ from calibre import (browser, __appname__, iswindows,
strftime, preferred_encoding, as_unicode strftime, preferred_encoding, as_unicode)
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString, CData, Tag from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString, CData, Tag
from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre import entity_to_unicode from calibre import entity_to_unicode

View File

@ -101,6 +101,7 @@ def get_custom_recipe_collection(*args):
if recipe_class is not None: if recipe_class is not None:
rmap['custom:%s'%id_] = recipe_class rmap['custom:%s'%id_] = recipe_class
except: except:
print 'Failed to load recipe from: %r'%fname
import traceback import traceback
traceback.print_exc() traceback.print_exc()
continue continue