mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sync to trunk.
This commit is contained in:
commit
833356d7e2
@ -16,6 +16,7 @@ __UseLife__ = True
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
Change Log:
|
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
|
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
|
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
|
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'\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'\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'\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'\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'\u5c08\u6b04 Columns', 'http://life.mingpao.com/cfm/dailynews2.cfm?Issue=' + dateStr +'&Category=ncolumn', 'ncl')
|
||||||
|
]:
|
||||||
articles = self.parse_section2(url, keystr)
|
articles = self.parse_section2(url, keystr)
|
||||||
if articles:
|
if articles:
|
||||||
feeds.append((title, 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):
|
with nested(open(opf_path, 'wb'), open(ncx_path, 'wb')) as (opf_file, ncx_file):
|
||||||
opf.render(opf_file, ncx_file)
|
opf.render(opf_file, ncx_file)
|
||||||
|
|
||||||
|
@ -70,9 +70,18 @@ author_sort_copy_method = 'comma'
|
|||||||
author_name_suffixes = ('Jr', 'Sr', 'Inc', 'Ph.D', 'Phd',
|
author_name_suffixes = ('Jr', 'Sr', 'Inc', 'Ph.D', 'Phd',
|
||||||
'MD', 'M.D', 'I', 'II', 'III', 'IV',
|
'MD', 'M.D', 'I', 'II', 'III', 'IV',
|
||||||
'Junior', 'Senior')
|
'Junior', 'Senior')
|
||||||
|
author_name_prefixes = ('Mr', 'Mrs', 'Ms', 'Dr', 'Prof')
|
||||||
author_name_copywords = ('Corporation', 'Company', 'Co.', 'Agency', 'Council',
|
author_name_copywords = ('Corporation', 'Company', 'Co.', 'Agency', 'Council',
|
||||||
'Committee', 'Inc.', 'Institute', 'Society', 'Club', 'Team')
|
'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
|
#: Use author sort in Tag Browser
|
||||||
# Set which author field to display in the tags pane (the list of authors,
|
# 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
|
# series, publishers etc on the left hand side). The choices are author and
|
||||||
|
@ -571,7 +571,7 @@ from calibre.devices.teclast.driver import (TECLAST_K3, NEWSMY, IPAPYRUS,
|
|||||||
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,
|
||||||
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.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||||
from calibre.devices.kobo.driver import KOBO
|
from calibre.devices.kobo.driver import KOBO
|
||||||
from calibre.devices.bambook.driver import BAMBOOK
|
from calibre.devices.bambook.driver import BAMBOOK
|
||||||
@ -707,7 +707,7 @@ plugins += [
|
|||||||
EEEREADER,
|
EEEREADER,
|
||||||
NEXTBOOK,
|
NEXTBOOK,
|
||||||
ADAM,
|
ADAM,
|
||||||
MOOVYBOOK, COBY,
|
MOOVYBOOK, COBY, EX124G,
|
||||||
ITUNES,
|
ITUNES,
|
||||||
BOEYE_BEX,
|
BOEYE_BEX,
|
||||||
BOEYE_BDX,
|
BOEYE_BDX,
|
||||||
|
@ -164,7 +164,7 @@ class ManyToOneField(Field):
|
|||||||
def for_book(self, book_id, default_value=None):
|
def for_book(self, book_id, default_value=None):
|
||||||
ids = self.table.book_col_map.get(book_id, None)
|
ids = self.table.book_col_map.get(book_id, None)
|
||||||
if ids is not None:
|
if ids is not None:
|
||||||
ans = self.id_map[ids]
|
ans = self.table.id_map[ids]
|
||||||
else:
|
else:
|
||||||
ans = default_value
|
ans = default_value
|
||||||
return ans
|
return ans
|
||||||
@ -182,7 +182,7 @@ class ManyToOneField(Field):
|
|||||||
return self.table.id_map.iterkeys()
|
return self.table.id_map.iterkeys()
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, all_book_ids):
|
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}
|
all_book_ids}
|
||||||
return {id_ : keys.get(
|
return {id_ : keys.get(
|
||||||
self.book_col_map.get(id_, None), '') for id_ in all_book_ids}
|
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):
|
def for_book(self, book_id, default_value=None):
|
||||||
ids = self.table.book_col_map.get(book_id, ())
|
ids = self.table.book_col_map.get(book_id, ())
|
||||||
if ids:
|
if ids:
|
||||||
ans = tuple(self.id_map[i] for i in ids)
|
ans = tuple(self.table.id_map[i] for i in ids)
|
||||||
else:
|
else:
|
||||||
ans = default_value
|
ans = default_value
|
||||||
return ans
|
return ans
|
||||||
@ -211,7 +211,7 @@ class ManyToManyField(Field):
|
|||||||
return self.table.id_map.iterkeys()
|
return self.table.id_map.iterkeys()
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, all_book_ids):
|
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}
|
all_book_ids}
|
||||||
|
|
||||||
def sort_key_for_book(book_id):
|
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}
|
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):
|
class AuthorsField(ManyToManyField):
|
||||||
|
|
||||||
@ -249,6 +256,8 @@ def create_field(name, table):
|
|||||||
cls = OnDeviceField
|
cls = OnDeviceField
|
||||||
elif name == 'formats':
|
elif name == 'formats':
|
||||||
cls = FormatsField
|
cls = FormatsField
|
||||||
|
elif name == 'identifiers':
|
||||||
|
cls = IdentifiersField
|
||||||
elif table.metadata['datatype'] == 'composite':
|
elif table.metadata['datatype'] == 'composite':
|
||||||
cls = CompositeField
|
cls = CompositeField
|
||||||
return cls(name, table)
|
return cls(name, table)
|
||||||
|
@ -66,8 +66,6 @@ class VirtualTable(Table):
|
|||||||
self.table_type = table_type
|
self.table_type = table_type
|
||||||
Table.__init__(self, name, metadata)
|
Table.__init__(self, name, metadata)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OneToOneTable(Table):
|
class OneToOneTable(Table):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
11
src/calibre/db/tests/__init__.py
Normal file
11
src/calibre/db/tests/__init__.py
Normal 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'
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
src/calibre/db/tests/metadata.db
Normal file
BIN
src/calibre/db/tests/metadata.db
Normal file
Binary file not shown.
116
src/calibre/db/tests/reading.py
Normal file
116
src/calibre/db/tests/reading.py
Normal 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()
|
||||||
|
|
@ -180,6 +180,7 @@ def main(args=sys.argv):
|
|||||||
sys.path.insert(0, base)
|
sys.path.insert(0, base)
|
||||||
g = globals()
|
g = globals()
|
||||||
g['__name__'] = '__main__'
|
g['__name__'] = '__main__'
|
||||||
|
g['__file__'] = ef
|
||||||
execfile(ef, g)
|
execfile(ef, g)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ class ANDROID(USBMS):
|
|||||||
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
||||||
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
||||||
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA',
|
'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',
|
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||||
@ -137,11 +137,11 @@ class ANDROID(USBMS):
|
|||||||
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
|
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
|
||||||
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB', 'STREAK',
|
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB', 'STREAK',
|
||||||
'MB525', 'ANDROID2.3', 'SGH-I997', 'GT-I5800_CARD', 'MB612',
|
'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',
|
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_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'
|
OSX_MAIN_MEM = 'Android Device Main Memory'
|
||||||
|
|
||||||
|
@ -377,3 +377,31 @@ class COBY(USBMS):
|
|||||||
return 'eBooks'
|
return 'eBooks'
|
||||||
return self.EBOOK_DIR_CARD_A
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -291,6 +291,8 @@ class PRS505(USBMS):
|
|||||||
thumbnail_dir = os.path.join(thumbnail_dir, relpath)
|
thumbnail_dir = os.path.join(thumbnail_dir, relpath)
|
||||||
if not os.path.exists(thumbnail_dir):
|
if not os.path.exists(thumbnail_dir):
|
||||||
os.makedirs(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])
|
f.write(metadata.thumbnail[-1])
|
||||||
|
debug_print('Cover uploaded to: %r'%cpath)
|
||||||
|
|
||||||
|
@ -636,12 +636,3 @@ class HTMLPreProcessor(object):
|
|||||||
html = re.sub(r'\s--\s', u'\u2014', html)
|
html = re.sub(r'\s--\s', u'\u2014', html)
|
||||||
return substitute_entites(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)
|
|
||||||
|
|
||||||
|
@ -10,11 +10,17 @@ import os, sys, re
|
|||||||
from urllib import unquote, quote
|
from urllib import unquote, quote
|
||||||
from urlparse import urlparse
|
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
|
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):
|
def string_to_authors(raw):
|
||||||
raw = raw.replace('&&', u'\uffff')
|
raw = raw.replace('&&', u'\uffff')
|
||||||
raw = _author_pat.sub('&', raw)
|
raw = _author_pat.sub('&', raw)
|
||||||
@ -45,6 +51,17 @@ def author_to_author_sort(author, method=None):
|
|||||||
if method == u'copy':
|
if method == u'copy':
|
||||||
return author
|
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.lower() for x in tweaks['author_name_suffixes']])
|
||||||
suffixes |= set([x+u'.' for x in suffixes])
|
suffixes |= set([x+u'.' for x in suffixes])
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ from calibre.ebooks.metadata.opf2 import OPF
|
|||||||
from calibre.ptempfile import TemporaryDirectory, PersistentTemporaryFile
|
from calibre.ptempfile import TemporaryDirectory, PersistentTemporaryFile
|
||||||
from calibre import CurrentDir, walk
|
from calibre import CurrentDir, walk
|
||||||
from calibre.constants import isosx
|
from calibre.constants import isosx
|
||||||
|
from calibre.utils.localization import lang_as_iso639_1
|
||||||
|
|
||||||
class EPubException(Exception):
|
class EPubException(Exception):
|
||||||
pass
|
pass
|
||||||
@ -231,6 +232,15 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
|
|||||||
|
|
||||||
for x in ('guide', 'toc', 'manifest', 'spine'):
|
for x in ('guide', 'toc', 'manifest', 'spine'):
|
||||||
setattr(mi, x, None)
|
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)
|
reader.opf.smart_update(mi)
|
||||||
if apply_null:
|
if apply_null:
|
||||||
if not getattr(mi, 'series', None):
|
if not getattr(mi, 'series', None):
|
||||||
|
@ -74,6 +74,20 @@ class Worker(Thread): # Get details {{{
|
|||||||
9: ['sept'],
|
9: ['sept'],
|
||||||
12: ['déc'],
|
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()="Produktinformation" or \
|
||||||
text()="Dettagli prodotto" or \
|
text()="Dettagli prodotto" or \
|
||||||
text()="Product details" 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 = '''
|
self.publisher_xpath = '''
|
||||||
descendant::*[starts-with(text(), "Publisher:") or \
|
descendant::*[starts-with(text(), "Publisher:") or \
|
||||||
starts-with(text(), "Verlag:") or \
|
starts-with(text(), "Verlag:") or \
|
||||||
starts-with(text(), "Editore:") or \
|
starts-with(text(), "Editore:") or \
|
||||||
starts-with(text(), "Editeur")]
|
starts-with(text(), "Editeur") or \
|
||||||
|
starts-with(text(), "出版社:")]
|
||||||
'''
|
'''
|
||||||
self.language_xpath = '''
|
self.language_xpath = '''
|
||||||
descendant::*[
|
descendant::*[
|
||||||
@ -101,10 +117,11 @@ class Worker(Thread): # Get details {{{
|
|||||||
or text() = "Sprache:" \
|
or text() = "Sprache:" \
|
||||||
or text() = "Lingua:" \
|
or text() = "Lingua:" \
|
||||||
or starts-with(text(), "Langue") \
|
or starts-with(text(), "Langue") \
|
||||||
|
or starts-with(text(), "言語") \
|
||||||
]
|
]
|
||||||
'''
|
'''
|
||||||
self.ratings_pat = re.compile(
|
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 = {
|
lm = {
|
||||||
'eng': ('English', 'Englisch'),
|
'eng': ('English', 'Englisch'),
|
||||||
@ -112,6 +129,7 @@ class Worker(Thread): # Get details {{{
|
|||||||
'ita': ('Italian', 'Italiano'),
|
'ita': ('Italian', 'Italiano'),
|
||||||
'deu': ('German', 'Deutsch'),
|
'deu': ('German', 'Deutsch'),
|
||||||
'spa': ('Spanish', 'Espa\xf1ol', 'Espaniol'),
|
'spa': ('Spanish', 'Espa\xf1ol', 'Espaniol'),
|
||||||
|
'jpn': ('Japanese', u'日本語'),
|
||||||
}
|
}
|
||||||
self.lang_map = {}
|
self.lang_map = {}
|
||||||
for code, names in lm.iteritems():
|
for code, names in lm.iteritems():
|
||||||
@ -403,6 +421,7 @@ class Amazon(Source):
|
|||||||
'de' : _('Germany'),
|
'de' : _('Germany'),
|
||||||
'uk' : _('UK'),
|
'uk' : _('UK'),
|
||||||
'it' : _('Italy'),
|
'it' : _('Italy'),
|
||||||
|
'jp' : _('Japan'),
|
||||||
}
|
}
|
||||||
|
|
||||||
options = (
|
options = (
|
||||||
@ -411,6 +430,22 @@ class Amazon(Source):
|
|||||||
'country\'s Amazon website.'), choices=AMAZON_DOMAINS),
|
'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):
|
def get_domain_and_asin(self, identifiers):
|
||||||
for key, val in identifiers.iteritems():
|
for key, val in identifiers.iteritems():
|
||||||
key = key.lower()
|
key = key.lower()
|
||||||
@ -488,13 +523,23 @@ class Amazon(Source):
|
|||||||
# Insufficient metadata to make an identify query
|
# Insufficient metadata to make an identify query
|
||||||
return None, None
|
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
|
'ignore')) for x, y in
|
||||||
q.iteritems()])
|
q.iteritems()])
|
||||||
udomain = domain
|
udomain = domain
|
||||||
if domain == 'uk':
|
if domain == 'uk':
|
||||||
udomain = 'co.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
|
return url, domain
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
@ -663,7 +708,7 @@ if __name__ == '__main__': # tests {{{
|
|||||||
# To run these test use: calibre-debug -e
|
# To run these test use: calibre-debug -e
|
||||||
# src/calibre/ebooks/metadata/sources/amazon.py
|
# src/calibre/ebooks/metadata/sources/amazon.py
|
||||||
from calibre.ebooks.metadata.sources.test import (test_identify_plugin,
|
from calibre.ebooks.metadata.sources.test import (test_identify_plugin,
|
||||||
title_test, authors_test)
|
isbn_test, title_test, authors_test)
|
||||||
com_tests = [ # {{{
|
com_tests = [ # {{{
|
||||||
|
|
||||||
( # Description has links
|
( # 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, com_tests)
|
||||||
|
#test_identify_plugin(Amazon.name, jp_tests)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ class OEBOutput(OutputFormatPlugin):
|
|||||||
except:
|
except:
|
||||||
self.log.exception('Something went wrong while trying to'
|
self.log.exception('Something went wrong while trying to'
|
||||||
' workaround Pocketbook cover bug, ignoring')
|
' workaround Pocketbook cover bug, ignoring')
|
||||||
|
self.migrate_lang_code(root)
|
||||||
raw = etree.tostring(root, pretty_print=True,
|
raw = etree.tostring(root, pretty_print=True,
|
||||||
encoding='utf-8', xml_declaration=True)
|
encoding='utf-8', xml_declaration=True)
|
||||||
if key == OPF_MIME:
|
if key == OPF_MIME:
|
||||||
@ -104,3 +105,12 @@ class OEBOutput(OutputFormatPlugin):
|
|||||||
p.remove(m)
|
p.remove(m)
|
||||||
p.insert(0, 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
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -10,16 +10,22 @@ from calibre.ebooks.oeb.base import OEB_DOCS, XPath, barename
|
|||||||
from calibre.utils.unsmarten import unsmarten_text
|
from calibre.utils.unsmarten import unsmarten_text
|
||||||
|
|
||||||
class UnsmartenPunctuation(object):
|
class UnsmartenPunctuation(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.html_tags = XPath('descendant::h:*')
|
||||||
|
|
||||||
def unsmarten(self, root):
|
def unsmarten(self, root):
|
||||||
for x in XPath('//h:*')(root):
|
for x in self.html_tags(root):
|
||||||
if not barename(x) == 'pre':
|
if not barename(x) == 'pre':
|
||||||
if hasattr(x, 'text') and x.text:
|
if getattr(x, 'text', None):
|
||||||
x.text = unsmarten_text(x.text)
|
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)
|
x.tail = unsmarten_text(x.tail)
|
||||||
|
|
||||||
def __call__(self, oeb, context):
|
def __call__(self, oeb, context):
|
||||||
|
bx = XPath('//h:body')
|
||||||
for x in oeb.manifest.items:
|
for x in oeb.manifest.items:
|
||||||
if x.media_type in OEB_DOCS:
|
if x.media_type in OEB_DOCS:
|
||||||
self.unsmarten(x.data)
|
for body in bx(x.data):
|
||||||
|
self.unsmarten(body)
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ class TextileMLizer(OEB2HTML):
|
|||||||
for i in self.our_ids:
|
for i in self.our_ids:
|
||||||
if i not in self.our_links:
|
if i not in self.our_links:
|
||||||
text = re.sub(r'%?\('+i+'\)\xa0?%?', r'', text)
|
text = re.sub(r'%?\('+i+'\)\xa0?%?', r'', text)
|
||||||
|
|
||||||
# Remove obvious non-needed escaping, add sub/sup-script ones
|
# Remove obvious non-needed escaping, add sub/sup-script ones
|
||||||
text = check_escaping(text, ['\*', '_', '\*'])
|
text = check_escaping(text, ['\*', '_', '\*'])
|
||||||
# escape the super/sub-scripts if needed
|
# escape the super/sub-scripts if needed
|
||||||
@ -189,7 +189,7 @@ class TextileMLizer(OEB2HTML):
|
|||||||
emright = int(round(right / stylizer.profile.fbase))
|
emright = int(round(right / stylizer.profile.fbase))
|
||||||
if emright >= 1:
|
if emright >= 1:
|
||||||
txt += ')' * emright
|
txt += ')' * emright
|
||||||
|
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def check_id_tag(self, attribs):
|
def check_id_tag(self, attribs):
|
||||||
@ -235,7 +235,7 @@ class TextileMLizer(OEB2HTML):
|
|||||||
tags = []
|
tags = []
|
||||||
tag = barename(elem.tag)
|
tag = barename(elem.tag)
|
||||||
attribs = elem.attrib
|
attribs = elem.attrib
|
||||||
|
|
||||||
# Ignore anything that is set to not be displayed.
|
# Ignore anything that is set to not be displayed.
|
||||||
if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \
|
if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \
|
||||||
or style['visibility'] == 'hidden':
|
or style['visibility'] == 'hidden':
|
||||||
@ -246,7 +246,7 @@ class TextileMLizer(OEB2HTML):
|
|||||||
ems = int(round(float(style.marginTop) / style.fontSize) - 1)
|
ems = int(round(float(style.marginTop) / style.fontSize) - 1)
|
||||||
if ems >= 1:
|
if ems >= 1:
|
||||||
text.append(u'\n\n\xa0' * ems)
|
text.append(u'\n\n\xa0' * ems)
|
||||||
|
|
||||||
if tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div'):
|
if tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div'):
|
||||||
if tag == 'div':
|
if tag == 'div':
|
||||||
tag = 'p'
|
tag = 'p'
|
||||||
@ -432,7 +432,7 @@ class TextileMLizer(OEB2HTML):
|
|||||||
'span', 'table', 'tr', 'td'):
|
'span', 'table', 'tr', 'td'):
|
||||||
if not self.in_a_link:
|
if not self.in_a_link:
|
||||||
text.append(self.check_styles(style))
|
text.append(self.check_styles(style))
|
||||||
|
|
||||||
# Process tags that contain text.
|
# Process tags that contain text.
|
||||||
if hasattr(elem, 'text') and elem.text:
|
if hasattr(elem, 'text') and elem.text:
|
||||||
txt = elem.text
|
txt = elem.text
|
||||||
|
@ -15,6 +15,8 @@ from calibre import prints
|
|||||||
from calibre.gui2 import Dispatcher
|
from calibre.gui2 import Dispatcher
|
||||||
from calibre.gui2.keyboard import NameConflict
|
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):
|
class InterfaceAction(QObject):
|
||||||
|
|
||||||
@ -178,30 +180,30 @@ class InterfaceAction(QObject):
|
|||||||
description=None, triggered=None, shortcut_name=None):
|
description=None, triggered=None, shortcut_name=None):
|
||||||
'''
|
'''
|
||||||
Convenience method to easily add actions to a QMenu.
|
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 menu: The QMenu the newly created action will be added to
|
||||||
:param unique_name: A unique name for this action, this must be
|
: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
|
globally unique, so make it as descriptive as possible. If in doubt add
|
||||||
a uuid to it.
|
a uuid to it.
|
||||||
:param text: The text of the action.
|
:param text: The text of the action.
|
||||||
:param icon: Either a QIcon or a file name. The file name is passed to
|
: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
|
the I() builtin, so you do not need to pass the full path to the images
|
||||||
directory.
|
directory.
|
||||||
:param shortcut: A string, a list of strings, None or False. If False,
|
: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
|
no keyboard shortcut is registered for this action. If None, a keyboard
|
||||||
shortcut with no default keybinding is registered. String and list of
|
shortcut with no default keybinding is registered. String and list of
|
||||||
strings register a shortcut with default keybinding as specified.
|
strings register a shortcut with default keybinding as specified.
|
||||||
:param description: A description for this action. Used to set
|
:param description: A description for this action. Used to set
|
||||||
tooltips.
|
tooltips.
|
||||||
:param triggered: A callable which is connected to the triggered signal
|
: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
|
:param shortcut_name: The test displayed to the user when customizing
|
||||||
the keyboard shortcuts for this action. By default it is set to the
|
the keyboard shortcuts for this action. By default it is set to the
|
||||||
value of ``text``.
|
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:
|
if shortcut_name is None:
|
||||||
shortcut_name = unicode(text)
|
shortcut_name = unicode(text)
|
||||||
@ -214,7 +216,7 @@ class InterfaceAction(QObject):
|
|||||||
if shortcut is not None and shortcut is not False:
|
if shortcut is not None and shortcut is not False:
|
||||||
keys = ((shortcut,) if isinstance(shortcut, basestring) else
|
keys = ((shortcut,) if isinstance(shortcut, basestring) else
|
||||||
tuple(shortcut))
|
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:
|
if description is not None:
|
||||||
ac.setToolTip(description)
|
ac.setToolTip(description)
|
||||||
ac.setStatusTip(description)
|
ac.setStatusTip(description)
|
||||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__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.look_and_feel_ui import Ui_Form
|
||||||
from calibre.gui2.convert import Widget
|
from calibre.gui2.convert import Widget
|
||||||
@ -45,6 +45,12 @@ class LookAndFeelWidget(Widget, Ui_Form):
|
|||||||
self.font_key_wizard)
|
self.font_key_wizard)
|
||||||
self.opt_remove_paragraph_spacing.toggle()
|
self.opt_remove_paragraph_spacing.toggle()
|
||||||
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):
|
def get_value_handler(self, g):
|
||||||
if g is self.opt_change_justification:
|
if g is self.opt_change_justification:
|
||||||
|
@ -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 import Dispatcher, error_dialog, question_dialog, NONE, config, gprefs
|
||||||
from calibre.gui2.device import DeviceJob
|
from calibre.gui2.device import DeviceJob
|
||||||
from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
|
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.dialogs.job_view_ui import Ui_Dialog
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
from calibre.gui2.threaded_jobs import ThreadedJobServer, ThreadedJob
|
from calibre.gui2.threaded_jobs import ThreadedJobServer, ThreadedJob
|
||||||
@ -264,6 +264,26 @@ class JobManager(QAbstractTableModel): # {{{
|
|||||||
_('This job cannot be stopped'), show=True)
|
_('This job cannot be stopped'), show=True)
|
||||||
self._kill_job(job)
|
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):
|
def kill_all_jobs(self):
|
||||||
for job in self.jobs:
|
for job in self.jobs:
|
||||||
if (isinstance(job, DeviceJob) or job.duration is not None or
|
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?',
|
ngettext('Do you really want to stop the selected job?',
|
||||||
'Do you really want to stop all the selected jobs?',
|
'Do you really want to stop all the selected jobs?',
|
||||||
len(rows))):
|
len(rows))):
|
||||||
for row in rows:
|
if len(rows) > 1:
|
||||||
self.model.kill_job(row, self)
|
self.model.kill_multiple_jobs(rows, self)
|
||||||
|
else:
|
||||||
|
self.model.kill_job(rows[0], self)
|
||||||
|
|
||||||
def kill_all_jobs(self, *args):
|
def kill_all_jobs(self, *args):
|
||||||
if question_dialog(self, _('Are you sure?'),
|
if question_dialog(self, _('Are you sure?'),
|
||||||
|
@ -116,6 +116,11 @@ class Manager(QObject): # {{{
|
|||||||
done unregistering.
|
done unregistering.
|
||||||
'''
|
'''
|
||||||
self.shortcuts.pop(unique_name, None)
|
self.shortcuts.pop(unique_name, None)
|
||||||
|
for group in self.groups.itervalues():
|
||||||
|
try:
|
||||||
|
group.remove(unique_name)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
def finalize(self):
|
def finalize(self):
|
||||||
custom_keys_map = {un:tuple(keys) for un, keys in self.config.get(
|
custom_keys_map = {un:tuple(keys) for un, keys in self.config.get(
|
||||||
|
@ -235,11 +235,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.highlight_index(idx)
|
self.highlight_index(idx)
|
||||||
|
|
||||||
def highlight_index(self, idx):
|
def highlight_index(self, idx):
|
||||||
self.plugin_view.scrollTo(idx)
|
|
||||||
self.plugin_view.selectionModel().select(idx,
|
self.plugin_view.selectionModel().select(idx,
|
||||||
self.plugin_view.selectionModel().ClearAndSelect)
|
self.plugin_view.selectionModel().ClearAndSelect)
|
||||||
self.plugin_view.setCurrentIndex(idx)
|
self.plugin_view.setCurrentIndex(idx)
|
||||||
self.plugin_view.setFocus(Qt.OtherFocusReason)
|
self.plugin_view.setFocus(Qt.OtherFocusReason)
|
||||||
|
self.plugin_view.scrollTo(idx, self.plugin_view.EnsureVisible)
|
||||||
|
|
||||||
def find_next(self, *args):
|
def find_next(self, *args):
|
||||||
idx = self.plugin_view.currentIndex()
|
idx = self.plugin_view.currentIndex()
|
||||||
|
@ -5113,6 +5113,8 @@ Author '{0}':
|
|||||||
|
|
||||||
if catalog_source_built:
|
if catalog_source_built:
|
||||||
recommendations = []
|
recommendations = []
|
||||||
|
recommendations.append(('remove_fake_margins', False,
|
||||||
|
OptionRecommendation.HIGH))
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
recommendations.append(('comments', '\n'.join(line for line in build_log),
|
recommendations.append(('comments', '\n'.join(line for line in build_log),
|
||||||
OptionRecommendation.HIGH))
|
OptionRecommendation.HIGH))
|
||||||
|
@ -6,7 +6,9 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, time
|
import os, time, glob
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
from sphinx.builders.epub import EpubBuilder
|
from sphinx.builders.epub import EpubBuilder
|
||||||
|
|
||||||
@ -55,4 +57,38 @@ class EPUBHelpBuilder(EpubBuilder):
|
|||||||
def build_epub(self, outdir, *args, **kwargs):
|
def build_epub(self, outdir, *args, **kwargs):
|
||||||
if self.config.epub_cover:
|
if self.config.epub_cover:
|
||||||
self.add_cover(outdir, self.config.epub_cover)
|
self.add_cover(outdir, self.config.epub_cover)
|
||||||
|
self.fix_duplication_bugs(outdir)
|
||||||
EpubBuilder.build_epub(self, outdir, *args, **kwargs)
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,26 +7,32 @@ import re
|
|||||||
from UserDict import UserDict
|
from UserDict import UserDict
|
||||||
|
|
||||||
class MReplace(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.re = None
|
||||||
self.regex = None
|
self.regex = None
|
||||||
|
self.case_sensitive = case_sensitive
|
||||||
self.compile_regex()
|
self.compile_regex()
|
||||||
|
|
||||||
def compile_regex(self):
|
def compile_regex(self):
|
||||||
if len(self.data) > 0:
|
if len(self.data) > 0:
|
||||||
keys = sorted(self.data.keys(), key=len)
|
keys = sorted(self.data.keys(), key=len)
|
||||||
keys.reverse()
|
keys.reverse()
|
||||||
tmp = "(%s)" % "|".join(map(re.escape, keys))
|
tmp = "(%s)" % "|".join(map(re.escape, keys))
|
||||||
if self.re != tmp:
|
if self.re != tmp:
|
||||||
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):
|
def __call__(self, mo):
|
||||||
return self[mo.string[mo.start():mo.end()]]
|
return self[mo.string[mo.start():mo.end()]]
|
||||||
|
|
||||||
def mreplace(self, text):
|
def mreplace(self, text):
|
||||||
#Replace without regex compile
|
#Replace without regex compile
|
||||||
if len(self.data) < 1 or self.re is None:
|
if len(self.data) < 1 or self.re is None:
|
||||||
return text
|
return text
|
||||||
return self.regex.sub(self, text)
|
return self.regex.sub(self, text)
|
||||||
|
|
||||||
|
@ -6,15 +6,38 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re
|
from calibre.utils.mreplace import MReplace
|
||||||
|
|
||||||
def unsmarten_text(txt):
|
_mreplace = MReplace({
|
||||||
txt = re.sub(u'–|–|–', r'--', txt) # en-dash
|
'–': '--',
|
||||||
txt = re.sub(u'—|—|—', r'---', txt) # em-dash
|
'–': '--',
|
||||||
txt = re.sub(u'…|…|…', r'...', txt) # ellipsis
|
'–': '--',
|
||||||
|
'—': '---',
|
||||||
|
'—': '---',
|
||||||
|
'—': '---',
|
||||||
|
'…': '...',
|
||||||
|
'…': '...',
|
||||||
|
'…': '...',
|
||||||
|
'“': '"',
|
||||||
|
'”': '"',
|
||||||
|
'″': '"',
|
||||||
|
'“': '"',
|
||||||
|
'”': '"',
|
||||||
|
'″': '"',
|
||||||
|
'“':'"',
|
||||||
|
'”':'"',
|
||||||
|
'″':'"',
|
||||||
|
'‘':"'",
|
||||||
|
'’':"'",
|
||||||
|
'′':"'",
|
||||||
|
'‘':"'",
|
||||||
|
'’':"'",
|
||||||
|
'′':"'",
|
||||||
|
'‘':"'",
|
||||||
|
'’':"'",
|
||||||
|
'′':"'",
|
||||||
|
})
|
||||||
|
|
||||||
txt = re.sub(u'“|”|″|“|”|″|“|”|″', r'"', txt) # double quote
|
unsmarten_text = _mreplace.mreplace
|
||||||
txt = re.sub(u'‘|’|′|‘|’|′|‘|’|′', r"'", txt) # single quote
|
|
||||||
|
|
||||||
return txt
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user