Sync to trunk.

This commit is contained in:
John Schember 2011-09-07 06:40:47 -04:00
commit 833356d7e2
28 changed files with 449 additions and 78 deletions

View File

@ -16,6 +16,7 @@ __UseLife__ = True
'''
Change Log:
2011/09/07: disable "column" section as it is no longer offered free.
2011/06/26: add fetching Vancouver and Toronto versions of the paper, also provide captions for images using life.mingpao fetch source
provide options to remove all images in the file
2011/05/12: switch the main parse source to life.mingpao.com, which has more photos on the article pages
@ -230,8 +231,9 @@ class MPRecipe(BasicNewsRecipe):
(u'\u570b\u969b World', 'http://life.mingpao.com/cfm/dailynews2.cfm?Issue=' + dateStr +'&Category=nalta', 'nal'),
(u'\u7d93\u6fdf Finance', 'http://life.mingpao.com/cfm/dailynews2.cfm?Issue=' + dateStr + '&Category=nalea', 'nal'),
(u'\u9ad4\u80b2 Sport', 'http://life.mingpao.com/cfm/dailynews2.cfm?Issue=' + dateStr + '&Category=nalsp', 'nal'),
(u'\u5f71\u8996 Film/TV', 'http://life.mingpao.com/cfm/dailynews2.cfm?Issue=' + dateStr + '&Category=nalma', 'nal'),
(u'\u5c08\u6b04 Columns', 'http://life.mingpao.com/cfm/dailynews2.cfm?Issue=' + dateStr +'&Category=ncolumn', 'ncl')]:
(u'\u5f71\u8996 Film/TV', 'http://life.mingpao.com/cfm/dailynews2.cfm?Issue=' + dateStr + '&Category=nalma', 'nal')
#(u'\u5c08\u6b04 Columns', 'http://life.mingpao.com/cfm/dailynews2.cfm?Issue=' + dateStr +'&Category=ncolumn', 'ncl')
]:
articles = self.parse_section2(url, keystr)
if articles:
feeds.append((title, articles))
@ -591,4 +593,3 @@ class MPRecipe(BasicNewsRecipe):
with nested(open(opf_path, 'wb'), open(ncx_path, 'wb')) as (opf_file, ncx_file):
opf.render(opf_file, ncx_file)

View File

@ -70,9 +70,18 @@ author_sort_copy_method = 'comma'
author_name_suffixes = ('Jr', 'Sr', 'Inc', 'Ph.D', 'Phd',
'MD', 'M.D', 'I', 'II', 'III', 'IV',
'Junior', 'Senior')
author_name_prefixes = ('Mr', 'Mrs', 'Ms', 'Dr', 'Prof')
author_name_copywords = ('Corporation', 'Company', 'Co.', 'Agency', 'Council',
'Committee', 'Inc.', 'Institute', 'Society', 'Club', 'Team')
#: Splitting multiple author names
# By default, calibre splits a string containing multiple author names on
# ampersands and the words "and" and "with". You can customize the splitting
# by changing the regular expression below. Strings are split on whatever the
# specified regular expression matches.
# Default: r'(?i),?\s+(and|with)\s+'
authors_split_regex = r'(?i),?\s+(and|with)\s+'
#: Use author sort in Tag Browser
# Set which author field to display in the tags pane (the list of authors,
# series, publishers etc on the left hand side). The choices are author and

View File

@ -571,7 +571,7 @@ from calibre.devices.teclast.driver import (TECLAST_K3, NEWSMY, IPAPYRUS,
from calibre.devices.sne.driver import SNE
from calibre.devices.misc import (PALMPRE, AVANT, SWEEX, PDNOVEL,
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, LUMIREAD, ALURATEK_COLOR,
TREKSTOR, EEEREADER, NEXTBOOK, ADAM, MOOVYBOOK, COBY)
TREKSTOR, EEEREADER, NEXTBOOK, ADAM, MOOVYBOOK, COBY, EX124G)
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK
@ -707,7 +707,7 @@ plugins += [
EEEREADER,
NEXTBOOK,
ADAM,
MOOVYBOOK, COBY,
MOOVYBOOK, COBY, EX124G,
ITUNES,
BOEYE_BEX,
BOEYE_BDX,

View File

@ -164,7 +164,7 @@ class ManyToOneField(Field):
def for_book(self, book_id, default_value=None):
ids = self.table.book_col_map.get(book_id, None)
if ids is not None:
ans = self.id_map[ids]
ans = self.table.id_map[ids]
else:
ans = default_value
return ans
@ -182,7 +182,7 @@ class ManyToOneField(Field):
return self.table.id_map.iterkeys()
def sort_keys_for_books(self, get_metadata, all_book_ids):
keys = {id_ : self._sort_key(self.id_map.get(id_, '')) for id_ in
keys = {id_ : self._sort_key(self.table.id_map.get(id_, '')) for id_ in
all_book_ids}
return {id_ : keys.get(
self.book_col_map.get(id_, None), '') for id_ in all_book_ids}
@ -196,7 +196,7 @@ class ManyToManyField(Field):
def for_book(self, book_id, default_value=None):
ids = self.table.book_col_map.get(book_id, ())
if ids:
ans = tuple(self.id_map[i] for i in ids)
ans = tuple(self.table.id_map[i] for i in ids)
else:
ans = default_value
return ans
@ -211,7 +211,7 @@ class ManyToManyField(Field):
return self.table.id_map.iterkeys()
def sort_keys_for_books(self, get_metadata, all_book_ids):
keys = {id_ : self._sort_key(self.id_map.get(id_, '')) for id_ in
keys = {id_ : self._sort_key(self.table.id_map.get(id_, '')) for id_ in
all_book_ids}
def sort_key_for_book(book_id):
@ -222,6 +222,13 @@ class ManyToManyField(Field):
return {id_ : sort_key_for_book(id_) for id_ in all_book_ids}
class IdentifiersField(ManyToManyField):
def for_book(self, book_id, default_value=None):
ids = self.table.book_col_map.get(book_id, ())
if not ids:
ids = default_value
return ids
class AuthorsField(ManyToManyField):
@ -249,6 +256,8 @@ def create_field(name, table):
cls = OnDeviceField
elif name == 'formats':
cls = FormatsField
elif name == 'identifiers':
cls = IdentifiersField
elif table.metadata['datatype'] == 'composite':
cls = CompositeField
return cls(name, table)

View File

@ -66,8 +66,6 @@ class VirtualTable(Table):
self.table_type = table_type
Table.__init__(self, name, metadata)
class OneToOneTable(Table):
'''

View File

@ -0,0 +1,11 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

Binary file not shown.

View File

@ -0,0 +1,116 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, shutil, unittest, tempfile, datetime
from calibre.utils.date import local_tz
def create_db(library_path):
from calibre.library.database2 import LibraryDatabase2
if LibraryDatabase2.exists_at(library_path):
raise ValueError('A library already exists at %r'%library_path)
src = os.path.join(os.path.dirname(__file__), 'metadata.db')
db = os.path.join(library_path, 'metadata.db')
shutil.copyfile(src, db)
return db
def init_cache(library_path):
from calibre.db.backend import DB
from calibre.db.cache import Cache
backend = DB(library_path)
cache = Cache(backend)
cache.init()
return cache
class ReadingTest(unittest.TestCase):
def setUp(self):
self.library_path = tempfile.mkdtemp()
create_db(self.library_path)
def tearDown(self):
shutil.rmtree(self.library_path)
def test_read(self): # {{{
cache = init_cache(self.library_path)
tests = {
2 : {
'title': 'Title One',
'sort': 'One',
'authors': ('Author One',),
'author_sort': 'One, Author',
'series' : 'Series One',
'series_index': 1.0,
'tags':('Tag Two', 'Tag One'),
'rating': 4.0,
'identifiers': {'test':'one'},
'timestamp': datetime.datetime(2011, 9, 5, 15, 6,
tzinfo=local_tz),
'pubdate': datetime.datetime(2011, 9, 5, 15, 6,
tzinfo=local_tz),
'publisher': 'Publisher One',
'languages': ('eng',),
'comments': '<p>Comments One</p>',
'#enum':'One',
'#authors':('Custom One', 'Custom Two'),
'#date':datetime.datetime(2011, 9, 5, 0, 0,
tzinfo=local_tz),
'#rating':2.0,
'#series':'My Series One',
'#series_index': 1.0,
'#tags':('My Tag One', 'My Tag Two'),
'#yesno':True,
'#comments': '<div>My Comments One<p></p></div>',
},
1 : {
'title': 'Title Two',
'sort': 'Title Two',
'authors': ('Author Two', 'Author One'),
'author_sort': 'Two, Author & One, Author',
'series' : 'Series Two',
'series_index': 2.0,
'rating': 6.0,
'tags': ('Tag Two',),
'identifiers': {'test':'two'},
'timestamp': datetime.datetime(2011, 9, 6, 0, 0,
tzinfo=local_tz),
'pubdate': datetime.datetime(2011, 8, 5, 0, 0,
tzinfo=local_tz),
'publisher': 'Publisher Two',
'languages': ('deu',),
'comments': '<p>Comments Two</p>',
'#enum':'Two',
'#authors':('My Author Two',),
'#date':datetime.datetime(2011, 9, 1, 0, 0,
tzinfo=local_tz),
'#rating':4.0,
'#series':'My Series Two',
'#series_index': 3.0,
'#tags':('My Tag Two',),
'#yesno':False,
'#comments': '<div>My Comments Two<p></p></div>',
},
}
for book_id, test in tests.iteritems():
for field, expected_val in test.iteritems():
self.assertEqual(expected_val,
cache.field_for(field, book_id))
# }}}
def tests():
return unittest.TestLoader().loadTestsFromTestCase(ReadingTest)
def run():
unittest.TextTestRunner(verbosity=2).run(tests())
if __name__ == '__main__':
run()

View File

@ -180,6 +180,7 @@ def main(args=sys.argv):
sys.path.insert(0, base)
g = globals()
g['__name__'] = '__main__'
g['__file__'] = ef
execfile(ef, g)
return

View File

@ -127,7 +127,7 @@ class ANDROID(USBMS):
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA',
'GENERIC-', 'ZTE', 'MID', 'QUALCOMM']
'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
@ -137,11 +137,11 @@ class ANDROID(USBMS):
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB', 'STREAK',
'MB525', 'ANDROID2.3', 'SGH-I997', 'GT-I5800_CARD', 'MB612',
'GT-S5830_CARD', 'GT-S5570_CARD', 'MB870', 'MID7015A']
'GT-S5830_CARD', 'GT-S5570_CARD', 'MB870', 'MID7015A', 'ALPANDIGITAL']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
'__UMS_COMPOSITE', 'SGH-I997_CARD', 'MB870']
'__UMS_COMPOSITE', 'SGH-I997_CARD', 'MB870', 'ALPANDIGITAL']
OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -377,3 +377,31 @@ class COBY(USBMS):
return 'eBooks'
return self.EBOOK_DIR_CARD_A
class EX124G(USBMS):
name = 'Motorola Ex124G device interface'
gui_name = 'Ex124G'
description = _('Communicate with the Ex124G')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['mobi', 'prc', 'azw']
VENDOR_ID = [0x0e8d]
PRODUCT_ID = [0x0002]
BCD = [0x0100]
VENDOR_NAME = 'MOTOROLA'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '_PHONE'
EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False
def get_carda_ebook_dir(self, for_upload=False):
if for_upload:
return 'eBooks'
return self.EBOOK_DIR_CARD_A

View File

@ -291,6 +291,8 @@ class PRS505(USBMS):
thumbnail_dir = os.path.join(thumbnail_dir, relpath)
if not os.path.exists(thumbnail_dir):
os.makedirs(thumbnail_dir)
with open(os.path.join(thumbnail_dir, 'main_thumbnail.jpg'), 'wb') as f:
cpath = os.path.join(thumbnail_dir, 'main_thumbnail.jpg')
with open(cpath, 'wb') as f:
f.write(metadata.thumbnail[-1])
debug_print('Cover uploaded to: %r'%cpath)

View File

@ -636,12 +636,3 @@ class HTMLPreProcessor(object):
html = re.sub(r'\s--\s', u'\u2014', html)
return substitute_entites(html)
def unsmarten_punctuation(self, html):
from calibre.utils.unsmarten import unsmarten_html
from calibre.ebooks.chardet import substitute_entites
from calibre.ebooks.conversion.utils import HeuristicProcessor
preprocessor = HeuristicProcessor(self.extra_opts, self.log)
html = preprocessor.fix_nbsp_indents(html)
html = unsmarten_html(html)
return substitute_entites(html)

View File

@ -10,11 +10,17 @@ import os, sys, re
from urllib import unquote, quote
from urlparse import urlparse
from calibre import relpath, guess_type, remove_bracketed_text
from calibre import relpath, guess_type, remove_bracketed_text, prints
from calibre.utils.config import tweaks
_author_pat = re.compile(',?\s+(and|with)\s+', re.IGNORECASE)
try:
_author_pat = re.compile(tweaks['authors_split_regex'])
except:
prints ('Author split regexp:', tweaks['authors_split_regex'],
'is invalid, using default')
_author_pat = re.compile(r'(?i),?\s+(and|with)\s+')
def string_to_authors(raw):
raw = raw.replace('&&', u'\uffff')
raw = _author_pat.sub('&', raw)
@ -45,6 +51,17 @@ def author_to_author_sort(author, method=None):
if method == u'copy':
return author
prefixes = set([x.lower() for x in tweaks['author_name_prefixes']])
prefixes |= set([x+u'.' for x in prefixes])
while True:
if not tokens:
return author
tok = tokens[0].lower()
if tok in prefixes:
tokens = tokens[1:]
else:
break
suffixes = set([x.lower() for x in tweaks['author_name_suffixes']])
suffixes |= set([x+u'.' for x in suffixes])

View File

@ -16,6 +16,7 @@ from calibre.ebooks.metadata.opf2 import OPF
from calibre.ptempfile import TemporaryDirectory, PersistentTemporaryFile
from calibre import CurrentDir, walk
from calibre.constants import isosx
from calibre.utils.localization import lang_as_iso639_1
class EPubException(Exception):
pass
@ -231,6 +232,15 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
for x in ('guide', 'toc', 'manifest', 'spine'):
setattr(mi, x, None)
if mi.languages:
langs = []
for lc in mi.languages:
lc2 = lang_as_iso639_1(lc)
if lc2: lc = lc2
langs.append(lc)
mi.languages = langs
reader.opf.smart_update(mi)
if apply_null:
if not getattr(mi, 'series', None):

View File

@ -74,6 +74,20 @@ class Worker(Thread): # Get details {{{
9: ['sept'],
12: ['déc'],
},
'jp': {
1: [u'1月'],
2: [u'2月'],
3: [u'3月'],
4: [u'4月'],
5: [u'5月'],
6: [u'6月'],
7: [u'7月'],
8: [u'8月'],
9: [u'9月'],
10: [u'10月'],
11: [u'11月'],
12: [u'12月'],
},
}
@ -86,13 +100,15 @@ class Worker(Thread): # Get details {{{
text()="Produktinformation" or \
text()="Dettagli prodotto" or \
text()="Product details" or \
text()="Détails sur le produit"]/../div[@class="content"]
text()="Détails sur le produit" or \
text()="登録情報"]/../div[@class="content"]
'''
self.publisher_xpath = '''
descendant::*[starts-with(text(), "Publisher:") or \
starts-with(text(), "Verlag:") or \
starts-with(text(), "Editore:") or \
starts-with(text(), "Editeur")]
starts-with(text(), "Editeur") or \
starts-with(text(), "出版社:")]
'''
self.language_xpath = '''
descendant::*[
@ -101,10 +117,11 @@ class Worker(Thread): # Get details {{{
or text() = "Sprache:" \
or text() = "Lingua:" \
or starts-with(text(), "Langue") \
or starts-with(text(), "言語") \
]
'''
self.ratings_pat = re.compile(
r'([0-9.]+) (out of|von|su|étoiles sur) (\d+)( (stars|Sternen|stelle)){0,1}')
r'([0-9.]+) ?(out of|von|su|étoiles sur|つ星のうち) ([\d\.]+)( (stars|Sternen|stelle)){0,1}')
lm = {
'eng': ('English', 'Englisch'),
@ -112,6 +129,7 @@ class Worker(Thread): # Get details {{{
'ita': ('Italian', 'Italiano'),
'deu': ('German', 'Deutsch'),
'spa': ('Spanish', 'Espa\xf1ol', 'Espaniol'),
'jpn': ('Japanese', u'日本語'),
}
self.lang_map = {}
for code, names in lm.iteritems():
@ -403,6 +421,7 @@ class Amazon(Source):
'de' : _('Germany'),
'uk' : _('UK'),
'it' : _('Italy'),
'jp' : _('Japan'),
}
options = (
@ -411,6 +430,22 @@ class Amazon(Source):
'country\'s Amazon website.'), choices=AMAZON_DOMAINS),
)
def __init__(self, *args, **kwargs):
Source.__init__(self, *args, **kwargs)
self.set_amazon_id_touched_fields()
def save_settings(self, *args, **kwargs):
Source.save_settings(self, *args, **kwargs)
self.set_amazon_id_touched_fields()
def set_amazon_id_touched_fields(self):
ident_name = "identifier:amazon"
if self.domain != 'com':
ident_name += '_' + self.domain
tf = [x for x in self.touched_fields if not
x.startswith('identifier:amazon')] + [ident_name]
self.touched_fields = frozenset(tf)
def get_domain_and_asin(self, identifiers):
for key, val in identifiers.iteritems():
key = key.lower()
@ -488,13 +523,23 @@ class Amazon(Source):
# Insufficient metadata to make an identify query
return None, None
latin1q = dict([(x.encode('latin1', 'ignore'), y.encode('latin1',
# magic parameter to enable Japanese Shift_JIS encoding.
if domain == 'jp':
q['__mk_ja_JP'] = u'カタカナ'
if domain == 'jp':
encode_to = 'Shift_JIS'
else:
encode_to = 'latin1'
encoded_q = dict([(x.encode(encode_to, 'ignore'), y.encode(encode_to,
'ignore')) for x, y in
q.iteritems()])
udomain = domain
if domain == 'uk':
udomain = 'co.uk'
url = 'http://www.amazon.%s/s/?'%udomain + urlencode(latin1q)
elif domain == 'jp':
udomain = 'co.jp'
url = 'http://www.amazon.%s/s/?'%udomain + urlencode(encoded_q)
return url, domain
# }}}
@ -663,7 +708,7 @@ if __name__ == '__main__': # tests {{{
# To run these test use: calibre-debug -e
# src/calibre/ebooks/metadata/sources/amazon.py
from calibre.ebooks.metadata.sources.test import (test_identify_plugin,
title_test, authors_test)
isbn_test, title_test, authors_test)
com_tests = [ # {{{
( # Description has links
@ -744,6 +789,21 @@ if __name__ == '__main__': # tests {{{
),
] # }}}
jp_tests = [ # {{{
( # isbn -> title, authors
{'identifiers':{'isbn': '9784101302720' }},
[title_test(u'精霊の守り人',
exact=True), authors_test([u'上橋 菜穂子'])
]
),
( # title, authors -> isbn (will use Shift_JIS encoding in query.)
{'title': u'考えない練習',
'authors': [u'小池 龍之介']},
[isbn_test('9784093881067'), ]
),
] # }}}
test_identify_plugin(Amazon.name, com_tests)
#test_identify_plugin(Amazon.name, jp_tests)
# }}}

View File

@ -43,6 +43,7 @@ class OEBOutput(OutputFormatPlugin):
except:
self.log.exception('Something went wrong while trying to'
' workaround Pocketbook cover bug, ignoring')
self.migrate_lang_code(root)
raw = etree.tostring(root, pretty_print=True,
encoding='utf-8', xml_declaration=True)
if key == OPF_MIME:
@ -104,3 +105,12 @@ class OEBOutput(OutputFormatPlugin):
p.remove(m)
p.insert(0, m)
# }}}
def migrate_lang_code(self, root): # {{{
from calibre.utils.localization import lang_as_iso639_1
for lang in root.xpath('//*[local-name() = "language"]'):
clc = lang_as_iso639_1(lang.text)
if clc:
lang.text = clc
# }}}

View File

@ -11,15 +11,21 @@ from calibre.utils.unsmarten import unsmarten_text
class UnsmartenPunctuation(object):
def __init__(self):
self.html_tags = XPath('descendant::h:*')
def unsmarten(self, root):
for x in XPath('//h:*')(root):
for x in self.html_tags(root):
if not barename(x) == 'pre':
if hasattr(x, 'text') and x.text:
if getattr(x, 'text', None):
x.text = unsmarten_text(x.text)
if hasattr(x, 'tail') and x.tail:
if getattr(x, 'tail', None) and x.tail:
x.tail = unsmarten_text(x.tail)
def __call__(self, oeb, context):
bx = XPath('//h:body')
for x in oeb.manifest.items:
if x.media_type in OEB_DOCS:
self.unsmarten(x.data)
for body in bx(x.data):
self.unsmarten(body)

View File

@ -15,6 +15,8 @@ from calibre import prints
from calibre.gui2 import Dispatcher
from calibre.gui2.keyboard import NameConflict
def menu_action_unique_name(plugin, unique_name):
return u'%s : menu action : %s'%(plugin.unique_name, unique_name)
class InterfaceAction(QObject):
@ -178,30 +180,30 @@ class InterfaceAction(QObject):
description=None, triggered=None, shortcut_name=None):
'''
Convenience method to easily add actions to a QMenu.
Returns the created QAction, This action has one extra attribute
calibre_shortcut_unique_name which if not None refers to the unique
name under which this action is registered with the keyboard manager.
:param menu: The QMenu the newly created action will be added to
:param unique_name: A unique name for this action, this must be
globally unique, so make it as descriptive as possible. If in doubt add
a uuid to it.
globally unique, so make it as descriptive as possible. If in doubt add
a uuid to it.
:param text: The text of the action.
:param icon: Either a QIcon or a file name. The file name is passed to
the I() builtin, so you do not need to pass the full path to the images
directory.
the I() builtin, so you do not need to pass the full path to the images
directory.
:param shortcut: A string, a list of strings, None or False. If False,
no keyboard shortcut is registered for this action. If None, a keyboard
shortcut with no default keybinding is registered. String and list of
strings register a shortcut with default keybinding as specified.
no keyboard shortcut is registered for this action. If None, a keyboard
shortcut with no default keybinding is registered. String and list of
strings register a shortcut with default keybinding as specified.
:param description: A description for this action. Used to set
tooltips.
tooltips.
:param triggered: A callable which is connected to the triggered signal
of the created action.
of the created action.
:param shortcut_name: The test displayed to the user when customizing
the keyboard shortcuts for this action. By default it is set to the
value of ``text``.
the keyboard shortcuts for this action. By default it is set to the
value of ``text``.
:return: The created QAction, This action has one extra attribute
calibre_shortcut_unique_name which if not None refers to the unique
name under which this action is registered with the keyboard manager.
'''
if shortcut_name is None:
shortcut_name = unicode(text)
@ -214,7 +216,7 @@ class InterfaceAction(QObject):
if shortcut is not None and shortcut is not False:
keys = ((shortcut,) if isinstance(shortcut, basestring) else
tuple(shortcut))
unique_name = '%s : menu action : %s'%(self.unique_name, unique_name)
unique_name = menu_action_unique_name(self, unique_name)
if description is not None:
ac.setToolTip(description)
ac.setStatusTip(description)

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import SIGNAL, QVariant
from PyQt4.Qt import SIGNAL, QVariant, Qt
from calibre.gui2.convert.look_and_feel_ui import Ui_Form
from calibre.gui2.convert import Widget
@ -45,6 +45,12 @@ class LookAndFeelWidget(Widget, Ui_Form):
self.font_key_wizard)
self.opt_remove_paragraph_spacing.toggle()
self.opt_remove_paragraph_spacing.toggle()
self.opt_smarten_punctuation.stateChanged.connect(
lambda state: state != Qt.Unchecked and
self.opt_unsmarten_punctuation.setCheckState(Qt.Unchecked))
self.opt_unsmarten_punctuation.stateChanged.connect(
lambda state: state != Qt.Unchecked and
self.opt_smarten_punctuation.setCheckState(Qt.Unchecked))
def get_value_handler(self, g):
if g is self.opt_change_justification:

View File

@ -21,7 +21,7 @@ from calibre.utils.ipc.job import ParallelJob
from calibre.gui2 import Dispatcher, error_dialog, question_dialog, NONE, config, gprefs
from calibre.gui2.device import DeviceJob
from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
from calibre import __appname__
from calibre import __appname__, as_unicode
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.threaded_jobs import ThreadedJobServer, ThreadedJob
@ -264,6 +264,26 @@ class JobManager(QAbstractTableModel): # {{{
_('This job cannot be stopped'), show=True)
self._kill_job(job)
def kill_multiple_jobs(self, rows, view):
jobs = [self.jobs[row] for row in rows]
devjobs = [j for j in jobs is isinstance(j, DeviceJob)]
if devjobs:
error_dialog(view, _('Cannot kill job'),
_('Cannot kill jobs that communicate with the device')).exec_()
jobs = [j for j in jobs if not isinstance(j, DeviceJob)]
jobs = [j for j in jobs if j.duration is None]
unkillable = [j for j in jobs if not getattr(j, 'killable', True)]
if unkillable:
names = u'\n'.join(as_unicode(j.description) for j in unkillable)
error_dialog(view, _('Cannot kill job'),
_('Some of the jobs cannot be stopped. Click Show details'
' to see the list of unstoppable jobs.'), det_msg=names,
show=True)
jobs = [j for j in jobs if getattr(j, 'killable', True)]
jobs = [j for j in jobs if j.duration is None]
for j in jobs:
self._kill_job(j)
def kill_all_jobs(self):
for job in self.jobs:
if (isinstance(job, DeviceJob) or job.duration is not None or
@ -484,8 +504,10 @@ class JobsDialog(QDialog, Ui_JobsDialog):
ngettext('Do you really want to stop the selected job?',
'Do you really want to stop all the selected jobs?',
len(rows))):
for row in rows:
self.model.kill_job(row, self)
if len(rows) > 1:
self.model.kill_multiple_jobs(rows, self)
else:
self.model.kill_job(rows[0], self)
def kill_all_jobs(self, *args):
if question_dialog(self, _('Are you sure?'),

View File

@ -116,6 +116,11 @@ class Manager(QObject): # {{{
done unregistering.
'''
self.shortcuts.pop(unique_name, None)
for group in self.groups.itervalues():
try:
group.remove(unique_name)
except ValueError:
pass
def finalize(self):
custom_keys_map = {un:tuple(keys) for un, keys in self.config.get(

View File

@ -235,11 +235,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.highlight_index(idx)
def highlight_index(self, idx):
self.plugin_view.scrollTo(idx)
self.plugin_view.selectionModel().select(idx,
self.plugin_view.selectionModel().ClearAndSelect)
self.plugin_view.setCurrentIndex(idx)
self.plugin_view.setFocus(Qt.OtherFocusReason)
self.plugin_view.scrollTo(idx, self.plugin_view.EnsureVisible)
def find_next(self, *args):
idx = self.plugin_view.currentIndex()

View File

@ -5113,6 +5113,8 @@ Author '{0}':
if catalog_source_built:
recommendations = []
recommendations.append(('remove_fake_margins', False,
OptionRecommendation.HIGH))
if DEBUG:
recommendations.append(('comments', '\n'.join(line for line in build_log),
OptionRecommendation.HIGH))

View File

@ -6,7 +6,9 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, time
import os, time, glob
from lxml import etree
from sphinx.builders.epub import EpubBuilder
@ -55,4 +57,38 @@ class EPUBHelpBuilder(EpubBuilder):
def build_epub(self, outdir, *args, **kwargs):
if self.config.epub_cover:
self.add_cover(outdir, self.config.epub_cover)
self.fix_duplication_bugs(outdir)
EpubBuilder.build_epub(self, outdir, *args, **kwargs)
def fix_duplication_bugs(self, outdir):
opf = glob.glob(outdir+os.sep+'*.opf')[0]
root = etree.fromstring(open(opf, 'rb').read())
seen = set()
for x in root.xpath(
'//*[local-name()="spine"]/*[local-name()="itemref"]'):
idref = x.get('idref')
if idref in seen:
x.getparent().remove(x)
else:
seen.add(idref)
with open(opf, 'wb') as f:
f.write(etree.tostring(root, encoding='utf-8', xml_declaration=True))
ncx = glob.glob(outdir+os.sep+'*.ncx')[0]
root = etree.fromstring(open(ncx, 'rb').read())
seen = set()
for x in root.xpath(
'//*[local-name()="navMap"]/*[local-name()="navPoint"]'):
text = x.xpath('descendant::*[local-name()="text"]')[0]
text = text.text
if text in seen:
x.getparent().remove(x)
else:
seen.add(text)
with open(ncx, 'wb') as f:
f.write(etree.tostring(root, encoding='utf-8', xml_declaration=True))

View File

@ -7,10 +7,12 @@ import re
from UserDict import UserDict
class MReplace(UserDict):
def __init__(self, dict = None):
UserDict.__init__(self, dict)
def __init__(self, data=None, case_sensitive=True):
UserDict.__init__(self, data)
self.re = None
self.regex = None
self.case_sensitive = case_sensitive
self.compile_regex()
def compile_regex(self):
@ -20,7 +22,10 @@ class MReplace(UserDict):
tmp = "(%s)" % "|".join(map(re.escape, keys))
if self.re != tmp:
self.re = tmp
self.regex = re.compile(self.re)
if self.case_sensitive:
self.regex = re.compile(self.re)
else:
self.regex = re.compile(self.re, re.I)
def __call__(self, mo):
return self[mo.string[mo.start():mo.end()]]
@ -30,3 +35,4 @@ class MReplace(UserDict):
if len(self.data) < 1 or self.re is None:
return text
return self.regex.sub(self, text)

View File

@ -6,15 +6,38 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import re
from calibre.utils.mreplace import MReplace
def unsmarten_text(txt):
txt = re.sub(u'&#8211;|&ndash;|', r'--', txt) # en-dash
txt = re.sub(u'&#8212;|&mdash;|—', r'---', txt) # em-dash
txt = re.sub(u'&#8230;|&hellip;|…', r'...', txt) # ellipsis
_mreplace = MReplace({
'&#8211;': '--',
'&ndash;': '--',
'': '--',
'&#8212;': '---',
'&mdash;': '---',
'': '---',
'&#8230;': '...',
'&hellip;': '...',
'': '...',
'&#8220;': '"',
'&#8221;': '"',
'&#8243;': '"',
'&ldquo;': '"',
'&rdquo;': '"',
'&Prime;': '"',
'':'"',
'':'"',
'':'"',
'&#8216;':"'",
'&#8217;':"'",
'&#8242;':"'",
'&lsquo;':"'",
'&rsquo;':"'",
'&prime;':"'",
'':"'",
'':"'",
'':"'",
})
txt = re.sub(u'&#8220;|&#8221;|&#8243;|&ldquo;|&rdquo;|&Prime;|“|”|″', r'"', txt) # double quote
txt = re.sub(u'&#8216;|&#8217;|&#8242;|&lsquo;|&rsquo;|&prime;|||', r"'", txt) # single quote
unsmarten_text = _mreplace.mreplace
return txt