Sync to trunk.

This commit is contained in:
John Schember 2013-04-23 20:12:31 -04:00
commit d18c934e32
14 changed files with 265 additions and 89 deletions

View File

@ -114,6 +114,19 @@ class Cache(object):
if self.dirtied_cache: if self.dirtied_cache:
self.dirtied_sequence = max(self.dirtied_cache.itervalues())+1 self.dirtied_sequence = max(self.dirtied_cache.itervalues())+1
@write_api
def initialize_template_cache(self):
self.formatter_template_cache = {}
@write_api
def refresh(self):
self._initialize_template_cache()
for field in self.fields.itervalues():
if hasattr(field, 'clear_cache'):
field.clear_cache() # Clear the composite cache
if hasattr(field, 'table'):
field.table.read(self.backend) # Reread data from metadata.db
@property @property
def field_metadata(self): def field_metadata(self):
return self.backend.field_metadata return self.backend.field_metadata

View File

@ -6,12 +6,13 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import os import os, traceback
from functools import partial from functools import partial
from calibre.db.backend import DB from calibre.db.backend import DB
from calibre.db.cache import Cache from calibre.db.cache import Cache
from calibre.db.view import View from calibre.db.view import View
from calibre.utils.date import utcnow
class LibraryDatabase(object): class LibraryDatabase(object):
@ -29,6 +30,7 @@ class LibraryDatabase(object):
progress_callback=lambda x, y:True, restore_all_prefs=False): progress_callback=lambda x, y:True, restore_all_prefs=False):
self.is_second_db = is_second_db # TODO: Use is_second_db self.is_second_db = is_second_db # TODO: Use is_second_db
self.listeners = set([])
backend = self.backend = DB(library_path, default_prefs=default_prefs, backend = self.backend = DB(library_path, default_prefs=default_prefs,
read_only=read_only, restore_all_prefs=restore_all_prefs, read_only=read_only, restore_all_prefs=restore_all_prefs,
@ -50,6 +52,8 @@ class LibraryDatabase(object):
setattr(self, prop, partial(self.get_property, setattr(self, prop, partial(self.get_property,
loc=self.FIELD_MAP[fm])) loc=self.FIELD_MAP[fm]))
self.last_update_check = self.last_modified()
def close(self): def close(self):
self.backend.close() self.backend.close()
@ -71,9 +75,22 @@ class LibraryDatabase(object):
def library_id(self): def library_id(self):
return self.backend.library_id return self.backend.library_id
@property
def library_path(self):
return self.backend.library_path
@property
def dbpath(self):
return self.backend.dbpath
def last_modified(self): def last_modified(self):
return self.backend.last_modified() return self.backend.last_modified()
def check_if_modified(self):
if self.last_modified() > self.last_update_check:
self.refresh()
self.last_update_check = utcnow()
@property @property
def custom_column_num_map(self): def custom_column_num_map(self):
return self.backend.custom_column_num_map return self.backend.custom_column_num_map
@ -86,9 +103,48 @@ class LibraryDatabase(object):
def FIELD_MAP(self): def FIELD_MAP(self):
return self.backend.FIELD_MAP return self.backend.FIELD_MAP
@property
def formatter_template_cache(self):
return self.data.cache.formatter_template_cache
def initialize_template_cache(self):
self.data.cache.initialize_template_cache()
def all_ids(self): def all_ids(self):
for book_id in self.data.cache.all_book_ids(): for book_id in self.data.cache.all_book_ids():
yield book_id yield book_id
def refresh(self, field=None, ascending=True):
self.data.cache.refresh()
self.data.refresh(field=field, ascending=ascending)
def add_listener(self, listener):
'''
Add a listener. Will be called on change events with two arguments.
Event name and list of affected ids.
'''
self.listeners.add(listener)
def notify(self, event, ids=[]):
'Notify all listeners'
for listener in self.listeners:
try:
listener(event, ids)
except:
traceback.print_exc()
continue
# }}} # }}}
def path(self, index, index_is_id=False):
'Return the relative path to the directory containing this books files as a unicode string.'
book_id = index if index_is_id else self.data.index_to_id(index)
return self.data.cache.field_for('path', book_id).replace('/', os.sep)
def abspath(self, index, index_is_id=False, create_dirs=True):
'Return the absolute path to the directory containing this books files as a unicode string.'
path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id))
if create_dirs and not os.path.exists(path):
os.makedirs(path)
return path

View File

@ -16,7 +16,7 @@ class LegacyTest(BaseTest):
'Test library wide properties' 'Test library wide properties'
def get_props(db): def get_props(db):
props = ('user_version', 'is_second_db', 'library_id', 'field_metadata', props = ('user_version', 'is_second_db', 'library_id', 'field_metadata',
'custom_column_label_map', 'custom_column_num_map') 'custom_column_label_map', 'custom_column_num_map', 'library_path', 'dbpath')
fprops = ('last_modified', ) fprops = ('last_modified', )
ans = {x:getattr(db, x) for x in props} ans = {x:getattr(db, x) for x in props}
ans.update({x:getattr(db, x)() for x in fprops}) ans.update({x:getattr(db, x)() for x in fprops})
@ -51,6 +51,11 @@ class LegacyTest(BaseTest):
if label in {'tags', 'formats'}: if label in {'tags', 'formats'}:
# Order is random in the old db for these # Order is random in the old db for these
ans[label] = tuple(set(x.split(',')) if x else x for x in ans[label]) ans[label] = tuple(set(x.split(',')) if x else x for x in ans[label])
if label == 'series_sort':
# The old db code did not take book language into account
# when generating series_sort values (the first book has
# lang=deu)
ans[label] = ans[label][1:]
return ans return ans
old = self.init_old() old = self.init_old()
@ -64,3 +69,31 @@ class LegacyTest(BaseTest):
# }}} # }}}
def test_refresh(self): # {{{
' Test refreshing the view after a change to metadata.db '
db = self.init_legacy()
db2 = self.init_legacy()
self.assertEqual(db2.data.cache.set_field('title', {1:'xxx'}), set([1]))
db2.close()
del db2
self.assertNotEqual(db.title(1, index_is_id=True), 'xxx')
db.check_if_modified()
self.assertEqual(db.title(1, index_is_id=True), 'xxx')
# }}}
def test_legacy_getters(self): # {{{
old = self.init_old()
getters = ('path', 'abspath', 'title', 'authors', 'series',
'publisher', 'author_sort', 'authors', 'comments',
'comment', 'publisher', 'rating', 'series_index', 'tags',
'timestamp', 'uuid', 'pubdate', 'ondevice',
'metadata_last_modified', 'languages')
oldvals = {g:tuple(getattr(old, g)(x) for x in xrange(3)) + tuple(getattr(old, g)(x, True) for x in (1,2,3)) for g in getters}
old.close()
db = self.init_legacy()
newvals = {g:tuple(getattr(db, g)(x) for x in xrange(3)) + tuple(getattr(db, g)(x, True) for x in (1,2,3)) for g in getters}
for x in (oldvals, newvals):
x['tags'] = tuple(set(y.split(',')) if y else y for y in x['tags'])
self.assertEqual(oldvals, newvals)
# }}}

View File

@ -294,3 +294,11 @@ class View(object):
self.marked_ids = dict(izip(id_dict.iterkeys(), imap(unicode, self.marked_ids = dict(izip(id_dict.iterkeys(), imap(unicode,
id_dict.itervalues()))) id_dict.itervalues())))
def refresh(self, field=None, ascending=True):
self._map = tuple(self.cache.all_book_ids())
self._map_filtered = tuple(self._map)
if field is not None:
self.sort(field, ascending)
if self.search_restriction or self.base_restriction:
self.search('', return_matches=False)

View File

@ -1199,9 +1199,9 @@ class KOBO(USBMS):
class KOBOTOUCH(KOBO): class KOBOTOUCH(KOBO):
name = 'KoboTouch' name = 'KoboTouch'
gui_name = 'Kobo Touch' gui_name = 'Kobo Touch/Glo/Mini/Aura HD'
author = 'David Forrester' author = 'David Forrester'
description = 'Communicate with the Kobo Touch, Glo and Mini firmware. Based on the existing Kobo driver by %s.' % (KOBO.author) description = 'Communicate with the Kobo Touch, Glo, Mini and Aura HD ereaders. Based on the existing Kobo driver by %s.' % (KOBO.author)
# icon = I('devices/kobotouch.jpg') # icon = I('devices/kobotouch.jpg')
supported_dbversion = 80 supported_dbversion = 80
@ -1297,12 +1297,13 @@ class KOBOTOUCH(KOBO):
TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ" TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ"
GLO_PRODUCT_ID = [0x4173] AURA_HD_PRODUCT_ID = [0x4193]
MINI_PRODUCT_ID = [0x4183] GLO_PRODUCT_ID = [0x4173]
TOUCH_PRODUCT_ID = [0x4163] MINI_PRODUCT_ID = [0x4183]
PRODUCT_ID = GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID TOUCH_PRODUCT_ID = [0x4163]
PRODUCT_ID = AURA_HD_PRODUCT_ID + GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID
BCD = [0x0110, 0x0326] BCD = [0x0110, 0x0326]
# Image file name endings. Made up of: image size, min_dbversion, max_dbversion, # Image file name endings. Made up of: image size, min_dbversion, max_dbversion,
COVER_FILE_ENDINGS = { COVER_FILE_ENDINGS = {
@ -1319,6 +1320,11 @@ class KOBOTOUCH(KOBO):
# ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,], # ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,],
# ' - N3_LIBRARY_SHELF.parsed': [(40,60),0, 52,], # ' - N3_LIBRARY_SHELF.parsed': [(40,60),0, 52,],
} }
AURA_HD_COVER_FILE_ENDINGS = {
' - N3_FULL.parsed': [(1080,1440), 0, 99,True,], # Used for screensaver, home screen
' - N3_LIBRARY_FULL.parsed':[(355, 471), 0, 99,False,], # Used for Details screen
' - N3_LIBRARY_GRID.parsed':[(149, 198), 0, 99,False,], # Used for library lists
}
#Following are the sizes used with pre2.1.4 firmware #Following are the sizes used with pre2.1.4 firmware
# COVER_FILE_ENDINGS = { # COVER_FILE_ENDINGS = {
# ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen # ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen
@ -1334,6 +1340,10 @@ class KOBOTOUCH(KOBO):
super(KOBOTOUCH, self).initialize() super(KOBOTOUCH, self).initialize()
self.bookshelvelist = [] self.bookshelvelist = []
def get_device_information(self, end_session=True):
self.set_device_name()
return super(KOBOTOUCH, self).get_device_information(end_session)
def books(self, oncard=None, end_session=True): def books(self, oncard=None, end_session=True):
debug_print("KoboTouch:books - oncard='%s'"%oncard) debug_print("KoboTouch:books - oncard='%s'"%oncard)
from calibre.ebooks.metadata.meta import path_to_ext from calibre.ebooks.metadata.meta import path_to_ext
@ -1366,7 +1376,7 @@ class KOBOTOUCH(KOBO):
except: except:
self.fwversion = (0,0,0) self.fwversion = (0,0,0)
debug_print('Kobo device: %s' % self.gui_name)
debug_print('Version of driver:', self.version, 'Has kepubs:', self.has_kepubs) debug_print('Version of driver:', self.version, 'Has kepubs:', self.has_kepubs)
debug_print('Version of firmware:', self.fwversion, 'Has kepubs:', self.has_kepubs) debug_print('Version of firmware:', self.fwversion, 'Has kepubs:', self.has_kepubs)
@ -1379,7 +1389,7 @@ class KOBOTOUCH(KOBO):
debug_print(opts.extra_customization) debug_print(opts.extra_customization)
if opts.extra_customization: if opts.extra_customization:
debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE]
debug_print("KoboTouch:books - set_debugging_title to", debugging_title ) debug_print("KoboTouch:books - set_debugging_title to '%s'" % debugging_title )
bl.set_debugging_title(debugging_title) bl.set_debugging_title(debugging_title)
debug_print("KoboTouch:books - length bl=%d"%len(bl)) debug_print("KoboTouch:books - length bl=%d"%len(bl))
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)
@ -1930,7 +1940,7 @@ class KOBOTOUCH(KOBO):
delete_empty_shelves = opts.extra_customization[self.OPT_DELETE_BOOKSHELVES] and self.supports_bookshelves() delete_empty_shelves = opts.extra_customization[self.OPT_DELETE_BOOKSHELVES] and self.supports_bookshelves()
update_series_details = opts.extra_customization[self.OPT_UPDATE_SERIES_DETAILS] and self.supports_series() update_series_details = opts.extra_customization[self.OPT_UPDATE_SERIES_DETAILS] and self.supports_series()
debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE]
debug_print("KoboTouch:update_device_database_collections - set_debugging_title to", debugging_title ) debug_print("KoboTouch:update_device_database_collections - set_debugging_title to '%s'" % debugging_title )
booklists.set_debugging_title(debugging_title) booklists.set_debugging_title(debugging_title)
else: else:
delete_empty_shelves = False delete_empty_shelves = False
@ -2516,6 +2526,8 @@ class KOBOTOUCH(KOBO):
return opts return opts
def isAuraHD(self):
return self.detected_device.idProduct in self.AURA_HD_PRODUCT_ID
def isGlo(self): def isGlo(self):
return self.detected_device.idProduct in self.GLO_PRODUCT_ID return self.detected_device.idProduct in self.GLO_PRODUCT_ID
def isMini(self): def isMini(self):
@ -2524,7 +2536,21 @@ class KOBOTOUCH(KOBO):
return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID
def cover_file_endings(self): def cover_file_endings(self):
return self.GLO_COVER_FILE_ENDINGS if self.isGlo() else self.COVER_FILE_ENDINGS return self.GLO_COVER_FILE_ENDINGS if self.isGlo() else self.AURA_HD_COVER_FILE_ENDINGS if self.isAuraHD() else self.COVER_FILE_ENDINGS
def set_device_name(self):
device_name = self.gui_name
if self.isAuraHD():
device_name = 'Kobo Aura HD'
elif self.isGlo():
device_name = 'Kobo Glo'
elif self.isMini():
device_name = 'Kobo Mini'
elif self.isTouch():
device_name = 'Kobo Touch'
self.__class__.gui_name = device_name
return device_name
def copying_covers(self): def copying_covers(self):
opts = self.settings() opts = self.settings()
@ -2582,14 +2608,6 @@ class KOBOTOUCH(KOBO):
# Supported database version # Supported database version
return True return True
# @classmethod
# def get_gui_name(cls):
# if hasattr(cls, 'gui_name'):
# return cls.gui_name
# if hasattr(cls, '__name__'):
# return cls.__name__
# return cls.name
@classmethod @classmethod
def is_debugging_title(cls, title): def is_debugging_title(cls, title):

View File

@ -95,7 +95,6 @@ class PDNOVEL(USBMS):
SUPPORTS_SUB_DIRS = False SUPPORTS_SUB_DIRS = False
DELETE_EXTS = ['.jpg', '.jpeg', '.png'] DELETE_EXTS = ['.jpg', '.jpeg', '.png']
def upload_cover(self, path, filename, metadata, filepath): def upload_cover(self, path, filename, metadata, filepath):
coverdata = getattr(metadata, 'thumbnail', None) coverdata = getattr(metadata, 'thumbnail', None)
if coverdata and coverdata[2]: if coverdata and coverdata[2]:
@ -226,9 +225,9 @@ class TREKSTOR(USBMS):
VENDOR_ID = [0x1e68] VENDOR_ID = [0x1e68]
PRODUCT_ID = [0x0041, 0x0042, 0x0052, 0x004e, 0x0056, PRODUCT_ID = [0x0041, 0x0042, 0x0052, 0x004e, 0x0056,
0x0067, # This is for the Pyrus Mini 0x0067, # This is for the Pyrus Mini
0x003e, # This is for the EBOOK_PLAYER_5M https://bugs.launchpad.net/bugs/792091 0x003e, # This is for the EBOOK_PLAYER_5M https://bugs.launchpad.net/bugs/792091
0x5cL, # This is for the 4ink http://www.mobileread.com/forums/showthread.php?t=191318 0x5cL, # This is for the 4ink http://www.mobileread.com/forums/showthread.php?t=191318
] ]
BCD = [0x0002, 0x100] BCD = [0x0002, 0x100]
@ -427,8 +426,8 @@ class WAYTEQ(USBMS):
EBOOK_DIR_MAIN = 'Documents' EBOOK_DIR_MAIN = 'Documents'
SCAN_FROM_ROOT = True SCAN_FROM_ROOT = True
VENDOR_NAME = 'ROCKCHIP' VENDOR_NAME = ['ROCKCHIP', 'CBR']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'RK28_SDK_DEMO' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['RK28_SDK_DEMO', 'EINK_EBOOK_READE']
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
def get_gui_name(self): def get_gui_name(self):
@ -445,7 +444,8 @@ class WAYTEQ(USBMS):
return self.EBOOK_DIR_CARD_A return self.EBOOK_DIR_CARD_A
def windows_sort_drives(self, drives): def windows_sort_drives(self, drives):
if len(drives) < 2: return drives if len(drives) < 2:
return drives
main = drives.get('main', None) main = drives.get('main', None)
carda = drives.get('carda', None) carda = drives.get('carda', None)
if main and carda: if main and carda:
@ -455,7 +455,8 @@ class WAYTEQ(USBMS):
def linux_swap_drives(self, drives): def linux_swap_drives(self, drives):
# See https://bugs.launchpad.net/bugs/1151901 # See https://bugs.launchpad.net/bugs/1151901
if len(drives) < 2 or not drives[1] or not drives[2]: return drives if len(drives) < 2 or not drives[1] or not drives[2]:
return drives
drives = list(drives) drives = list(drives)
t = drives[0] t = drives[0]
drives[0] = drives[1] drives[0] = drives[1]
@ -463,7 +464,8 @@ class WAYTEQ(USBMS):
return tuple(drives) return tuple(drives)
def osx_sort_names(self, names): def osx_sort_names(self, names):
if len(names) < 2: return names if len(names) < 2:
return names
main = names.get('main', None) main = names.get('main', None)
card = names.get('carda', None) card = names.get('carda', None)

View File

@ -58,8 +58,8 @@ class PICO(NEWSMY):
gui_name = 'Pico' gui_name = 'Pico'
description = _('Communicate with the Pico reader.') description = _('Communicate with the Pico reader.')
VENDOR_NAME = ['TECLAST', 'IMAGIN', 'LASER-', ''] VENDOR_NAME = ['TECLAST', 'IMAGIN', 'LASER-', 'LASER', '']
WINDOWS_MAIN_MEM = ['USBDISK__USER', 'EB720'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['USBDISK__USER', 'EB720', 'EBOOK-EB720']
EBOOK_DIR_MAIN = 'Books' EBOOK_DIR_MAIN = 'Books'
FORMATS = ['EPUB', 'FB2', 'TXT', 'LRC', 'PDB', 'PDF', 'HTML', 'WTXT'] FORMATS = ['EPUB', 'FB2', 'TXT', 'LRC', 'PDB', 'PDF', 'HTML', 'WTXT']
SCAN_FROM_ROOT = True SCAN_FROM_ROOT = True

View File

@ -188,7 +188,6 @@ class EPUBInput(InputFormatPlugin):
raise DRMError(os.path.basename(path)) raise DRMError(os.path.basename(path))
self.encrypted_fonts = self._encrypted_font_uris self.encrypted_fonts = self._encrypted_font_uris
if len(parts) > 1 and parts[0]: if len(parts) > 1 and parts[0]:
delta = '/'.join(parts[:-1])+'/' delta = '/'.join(parts[:-1])+'/'
for elem in opf.itermanifest(): for elem in opf.itermanifest():

View File

@ -1,7 +1,6 @@
''' '''
Basic support for manipulating OEB 1.x/2.0 content and metadata. Basic support for manipulating OEB 1.x/2.0 content and metadata.
''' '''
from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>' __copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
@ -11,7 +10,7 @@ import os, re, uuid, logging
from collections import defaultdict from collections import defaultdict
from itertools import count from itertools import count
from urlparse import urldefrag, urlparse, urlunparse, urljoin from urlparse import urldefrag, urlparse, urlunparse, urljoin
from urllib import unquote as urlunquote from urllib import unquote
from lxml import etree, html from lxml import etree, html
from calibre.constants import filesystem_encoding, __version__ from calibre.constants import filesystem_encoding, __version__
@ -40,11 +39,11 @@ CALIBRE_NS = 'http://calibre.kovidgoyal.net/2009/metadata'
RE_NS = 'http://exslt.org/regular-expressions' RE_NS = 'http://exslt.org/regular-expressions'
MBP_NS = 'http://www.mobipocket.com' MBP_NS = 'http://www.mobipocket.com'
XPNSMAP = {'h' : XHTML_NS, 'o1' : OPF1_NS, 'o2' : OPF2_NS, XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS,
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS, 'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
'xsi': XSI_NS, 'dt' : DCTERMS_NS, 'ncx': NCX_NS, 'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS,
'svg': SVG_NS, 'xl' : XLINK_NS, 're': RE_NS, 'svg': SVG_NS, 'xl': XLINK_NS, 're': RE_NS,
'mbp': MBP_NS, 'calibre': CALIBRE_NS } 'mbp': MBP_NS, 'calibre': CALIBRE_NS}
OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS} OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS}
OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS, OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS,
@ -142,7 +141,6 @@ def iterlinks(root, find_links_in_css=True):
if attr in link_attrs: if attr in link_attrs:
yield (el, attr, attribs[attr], 0) yield (el, attr, attribs[attr], 0)
if not find_links_in_css: if not find_links_in_css:
continue continue
if tag == XHTML('style') and el.text: if tag == XHTML('style') and el.text:
@ -363,7 +361,9 @@ URL_SAFE = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
URL_UNSAFE = [ASCII_CHARS - URL_SAFE, UNIBYTE_CHARS - URL_SAFE] URL_UNSAFE = [ASCII_CHARS - URL_SAFE, UNIBYTE_CHARS - URL_SAFE]
def urlquote(href): def urlquote(href):
"""Quote URL-unsafe characters, allowing IRI-safe characters.""" """ Quote URL-unsafe characters, allowing IRI-safe characters.
That is, this function returns valid IRIs not valid URIs. In particular,
IRIs can contain non-ascii characters. """
result = [] result = []
unsafe = 0 if isinstance(href, unicode) else 1 unsafe = 0 if isinstance(href, unicode) else 1
unsafe = URL_UNSAFE[unsafe] unsafe = URL_UNSAFE[unsafe]
@ -373,6 +373,19 @@ def urlquote(href):
result.append(char) result.append(char)
return ''.join(result) return ''.join(result)
def urlunquote(href):
# unquote must run on a bytestring and will return a bytestring
# If it runs on a unicode object, it returns a double encoded unicode
# string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8')
# and the latter is correct
want_unicode = isinstance(href, unicode)
if want_unicode:
href = href.encode('utf-8')
href = unquote(href)
if want_unicode:
href = href.decode('utf-8')
return href
def urlnormalize(href): def urlnormalize(href):
"""Convert a URL into normalized form, with all and only URL-unsafe """Convert a URL into normalized form, with all and only URL-unsafe
characters URL quoted. characters URL quoted.
@ -469,7 +482,7 @@ class DirContainer(object):
return return
def _unquote(self, path): def _unquote(self, path):
# urlunquote must run on a bytestring and will return a bytestring # unquote must run on a bytestring and will return a bytestring
# If it runs on a unicode object, it returns a double encoded unicode # If it runs on a unicode object, it returns a double encoded unicode
# string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8') # string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8')
# and the latter is correct # and the latter is correct
@ -497,7 +510,7 @@ class DirContainer(object):
return False return False
try: try:
path = os.path.join(self.rootdir, self._unquote(path)) path = os.path.join(self.rootdir, self._unquote(path))
except ValueError: #Happens if path contains quoted special chars except ValueError: # Happens if path contains quoted special chars
return False return False
try: try:
return os.path.isfile(path) return os.path.isfile(path)
@ -577,12 +590,13 @@ class Metadata(object):
allowed = self.allowed allowed = self.allowed
if allowed is not None and term not in allowed: if allowed is not None and term not in allowed:
raise AttributeError( raise AttributeError(
'attribute %r not valid for metadata term %r' \ 'attribute %r not valid for metadata term %r'
% (self.attr(term), barename(obj.term))) % (self.attr(term), barename(obj.term)))
return self.attr(term) return self.attr(term)
def __get__(self, obj, cls): def __get__(self, obj, cls):
if obj is None: return None if obj is None:
return None
return obj.attrib.get(self.term_attr(obj), '') return obj.attrib.get(self.term_attr(obj), '')
def __set__(self, obj, value): def __set__(self, obj, value):
@ -628,8 +642,8 @@ class Metadata(object):
self.value = value self.value = value
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
scheme = Attribute(lambda term: 'scheme' if \ scheme = Attribute(lambda term: 'scheme' if
term == OPF('meta') else OPF('scheme'), term == OPF('meta') else OPF('scheme'),
[DC('identifier'), OPF('meta')]) [DC('identifier'), OPF('meta')])
file_as = Attribute(OPF('file-as'), [DC('creator'), DC('contributor'), file_as = Attribute(OPF('file-as'), [DC('creator'), DC('contributor'),
DC('title')]) DC('title')])
@ -882,7 +896,6 @@ class Manifest(object):
return self._parse_xhtml(convert_markdown(data, title=title)) return self._parse_xhtml(convert_markdown(data, title=title))
def _parse_css(self, data): def _parse_css(self, data):
from cssutils import CSSParser, log, resolveImports from cssutils import CSSParser, log, resolveImports
log.setLevel(logging.WARN) log.setLevel(logging.WARN)
@ -935,7 +948,7 @@ class Manifest(object):
data = self._loader(getattr(self, 'html_input_href', data = self._loader(getattr(self, 'html_input_href',
self.href)) self.href))
if not isinstance(data, basestring): if not isinstance(data, basestring):
pass # already parsed pass # already parsed
elif self.media_type.lower() in OEB_DOCS: elif self.media_type.lower() in OEB_DOCS:
data = self._parse_xhtml(data) data = self._parse_xhtml(data)
elif self.media_type.lower()[-4:] in ('+xml', '/xml'): elif self.media_type.lower()[-4:] in ('+xml', '/xml'):
@ -1022,7 +1035,8 @@ class Manifest(object):
target, frag = urldefrag(href) target, frag = urldefrag(href)
target = target.split('/') target = target.split('/')
for index in xrange(min(len(base), len(target))): for index in xrange(min(len(base), len(target))):
if base[index] != target[index]: break if base[index] != target[index]:
break
else: else:
index += 1 index += 1
relhref = (['..'] * (len(base) - index)) + target[index:] relhref = (['..'] * (len(base) - index)) + target[index:]

View File

@ -148,7 +148,6 @@ class OEBReader(object):
if not has_aut: if not has_aut:
m.add('creator', self.oeb.translate(__('Unknown')), role='aut') m.add('creator', self.oeb.translate(__('Unknown')), role='aut')
def _manifest_prune_invalid(self): def _manifest_prune_invalid(self):
''' '''
Remove items from manifest that contain invalid data. This prevents Remove items from manifest that contain invalid data. This prevents
@ -197,6 +196,8 @@ class OEBReader(object):
item.media_type[-4:] in ('/xml', '+xml')): item.media_type[-4:] in ('/xml', '+xml')):
hrefs = [r[2] for r in iterlinks(data)] hrefs = [r[2] for r in iterlinks(data)]
for href in hrefs: for href in hrefs:
if isinstance(href, bytes):
href = href.decode('utf-8')
href, _ = urldefrag(href) href, _ = urldefrag(href)
if not href: if not href:
continue continue
@ -293,7 +294,7 @@ class OEBReader(object):
continue continue
try: try:
href = item.abshref(urlnormalize(href)) href = item.abshref(urlnormalize(href))
except ValueError: # Malformed URL except ValueError: # Malformed URL
continue continue
if href not in manifest.hrefs: if href not in manifest.hrefs:
continue continue
@ -394,9 +395,9 @@ class OEBReader(object):
authorElement = xpath(child, authorElement = xpath(child,
'descendant::calibre:meta[@name = "author"]') 'descendant::calibre:meta[@name = "author"]')
if authorElement : if authorElement:
author = authorElement[0].text author = authorElement[0].text
else : else:
author = None author = None
descriptionElement = xpath(child, descriptionElement = xpath(child,
@ -406,7 +407,7 @@ class OEBReader(object):
method='text', encoding=unicode).strip() method='text', encoding=unicode).strip()
if not description: if not description:
description = None description = None
else : else:
description = None description = None
index_image = xpath(child, index_image = xpath(child,
@ -497,7 +498,8 @@ class OEBReader(object):
titles = [] titles = []
headers = [] headers = []
for item in self.oeb.spine: for item in self.oeb.spine:
if not item.linear: continue if not item.linear:
continue
html = item.data html = item.data
title = ''.join(xpath(html, '/h:html/h:head/h:title/text()')) title = ''.join(xpath(html, '/h:html/h:head/h:title/text()'))
title = COLLAPSE_RE.sub(' ', title.strip()) title = COLLAPSE_RE.sub(' ', title.strip())
@ -515,17 +517,21 @@ class OEBReader(object):
if len(titles) > len(set(titles)): if len(titles) > len(set(titles)):
use = headers use = headers
for title, item in izip(use, self.oeb.spine): for title, item in izip(use, self.oeb.spine):
if not item.linear: continue if not item.linear:
continue
toc.add(title, item.href) toc.add(title, item.href)
return True return True
def _toc_from_opf(self, opf, item): def _toc_from_opf(self, opf, item):
self.oeb.auto_generated_toc = False self.oeb.auto_generated_toc = False
if self._toc_from_ncx(item): return if self._toc_from_ncx(item):
return
# Prefer HTML to tour based TOC, since several LIT files # Prefer HTML to tour based TOC, since several LIT files
# have good HTML TOCs but bad tour based TOCs # have good HTML TOCs but bad tour based TOCs
if self._toc_from_html(opf): return if self._toc_from_html(opf):
if self._toc_from_tour(opf): return return
if self._toc_from_tour(opf):
return
self._toc_from_spine(opf) self._toc_from_spine(opf)
self.oeb.auto_generated_toc = True self.oeb.auto_generated_toc = True
@ -589,8 +595,10 @@ class OEBReader(object):
return True return True
def _pages_from_opf(self, opf, item): def _pages_from_opf(self, opf, item):
if self._pages_from_ncx(opf, item): return if self._pages_from_ncx(opf, item):
if self._pages_from_page_map(opf): return return
if self._pages_from_page_map(opf):
return
return return
def _cover_from_html(self, hcover): def _cover_from_html(self, hcover):

View File

@ -47,6 +47,8 @@ class ManifestTrimmer(object):
item.data is not None: item.data is not None:
hrefs = [r[2] for r in iterlinks(item.data)] hrefs = [r[2] for r in iterlinks(item.data)]
for href in hrefs: for href in hrefs:
if isinstance(href, bytes):
href = href.decode('utf-8')
try: try:
href = item.abshref(urlnormalize(href)) href = item.abshref(urlnormalize(href))
except: except:

View File

@ -21,7 +21,7 @@ from PyQt4.Qt import (
QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget,
QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal, QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal,
QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex, QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex,
QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel) QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel, QMenu, QCursor)
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins from calibre.customize.ui import metadata_plugins
@ -40,7 +40,7 @@ from calibre.utils.ipc.simple_worker import fork_job, WorkerError
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
# }}} # }}}
class RichTextDelegate(QStyledItemDelegate): # {{{ class RichTextDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent=None, max_width=160): def __init__(self, parent=None, max_width=160):
QStyledItemDelegate.__init__(self, parent) QStyledItemDelegate.__init__(self, parent)
@ -77,7 +77,7 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
painter.restore() painter.restore()
# }}} # }}}
class CoverDelegate(QStyledItemDelegate): # {{{ class CoverDelegate(QStyledItemDelegate): # {{{
needs_redraw = pyqtSignal() needs_redraw = pyqtSignal()
@ -143,7 +143,7 @@ class CoverDelegate(QStyledItemDelegate): # {{{
# }}} # }}}
class ResultsModel(QAbstractTableModel): # {{{ class ResultsModel(QAbstractTableModel): # {{{
COLUMNS = ( COLUMNS = (
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary') '#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
@ -182,7 +182,6 @@ class ResultsModel(QAbstractTableModel): # {{{
p = book.publisher if book.publisher else '' p = book.publisher if book.publisher else ''
return '<b>%s</b><br><i>%s</i>' % (d, p) return '<b>%s</b><br><i>%s</i>' % (d, p)
def data(self, index, role): def data(self, index, role):
row, col = index.row(), index.column() row, col = index.row(), index.column()
try: try:
@ -233,7 +232,7 @@ class ResultsModel(QAbstractTableModel): # {{{
# }}} # }}}
class ResultsView(QTableView): # {{{ class ResultsView(QTableView): # {{{
show_details_signal = pyqtSignal(object) show_details_signal = pyqtSignal(object)
book_selected = pyqtSignal(object) book_selected = pyqtSignal(object)
@ -316,7 +315,7 @@ class ResultsView(QTableView): # {{{
# }}} # }}}
class Comments(QWebView): # {{{ class Comments(QWebView): # {{{
def __init__(self, parent=None): def __init__(self, parent=None):
QWebView.__init__(self, parent) QWebView.__init__(self, parent)
@ -384,7 +383,7 @@ class Comments(QWebView): # {{{
return QSize(800, 300) return QSize(800, 300)
# }}} # }}}
class IdentifyWorker(Thread): # {{{ class IdentifyWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers, caches): def __init__(self, log, abort, title, authors, identifiers, caches):
Thread.__init__(self) Thread.__init__(self)
@ -441,7 +440,7 @@ class IdentifyWorker(Thread): # {{{
# }}} # }}}
class IdentifyWidget(QWidget): # {{{ class IdentifyWidget(QWidget): # {{{
rejected = pyqtSignal() rejected = pyqtSignal()
results_found = pyqtSignal() results_found = pyqtSignal()
@ -552,12 +551,11 @@ class IdentifyWidget(QWidget): # {{{
self.results_view.show_results(self.worker.results) self.results_view.show_results(self.worker.results)
self.results_found.emit() self.results_found.emit()
def cancel(self): def cancel(self):
self.abort.set() self.abort.set()
# }}} # }}}
class CoverWorker(Thread): # {{{ class CoverWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers, caches): def __init__(self, log, abort, title, authors, identifiers, caches):
Thread.__init__(self) Thread.__init__(self)
@ -609,7 +607,8 @@ class CoverWorker(Thread): # {{{
def scan_once(self, tdir, seen): def scan_once(self, tdir, seen):
for x in list(os.listdir(tdir)): for x in list(os.listdir(tdir)):
if x in seen: continue if x in seen:
continue
if x.endswith('.cover') and os.path.exists(os.path.join(tdir, if x.endswith('.cover') and os.path.exists(os.path.join(tdir,
x+'.done')): x+'.done')):
name = x.rpartition('.')[0] name = x.rpartition('.')[0]
@ -635,7 +634,7 @@ class CoverWorker(Thread): # {{{
# }}} # }}}
class CoversModel(QAbstractListModel): # {{{ class CoversModel(QAbstractListModel): # {{{
def __init__(self, current_cover, parent=None): def __init__(self, current_cover, parent=None):
QAbstractListModel.__init__(self, parent) QAbstractListModel.__init__(self, parent)
@ -770,7 +769,7 @@ class CoversModel(QAbstractListModel): # {{{
# }}} # }}}
class CoversView(QListView): # {{{ class CoversView(QListView): # {{{
chosen = pyqtSignal() chosen = pyqtSignal()
@ -793,6 +792,8 @@ class CoversView(QListView): # {{{
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection) self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
def select(self, num): def select(self, num):
current = self.model().index(num) current = self.model().index(num)
@ -814,9 +815,24 @@ class CoversView(QListView): # {{{
else: else:
self.select(self.m.index_from_pointer(pointer).row()) self.select(self.m.index_from_pointer(pointer).row())
def show_context_menu(self, point):
idx = self.currentIndex()
if idx and idx.isValid() and not idx.data(Qt.UserRole).toPyObject():
m = QMenu()
m.addAction(QIcon(I('view.png')), _('View this cover at full size'), self.show_cover)
m.exec_(QCursor.pos())
def show_cover(self):
idx = self.currentIndex()
pmap = self.model().cover_pixmap(idx)
if pmap is not None:
from calibre.gui2.viewer.image_popup import ImageView
d = ImageView(self, pmap, unicode(idx.data(Qt.DisplayRole).toString()), geom_name='metadata_download_cover_popup_geom')
d(use_exec=True)
# }}} # }}}
class CoversWidget(QWidget): # {{{ class CoversWidget(QWidget): # {{{
chosen = pyqtSignal() chosen = pyqtSignal()
finished = pyqtSignal() finished = pyqtSignal()
@ -922,7 +938,7 @@ class CoversWidget(QWidget): # {{{
# }}} # }}}
class LogViewer(QDialog): # {{{ class LogViewer(QDialog): # {{{
def __init__(self, log, parent=None): def __init__(self, log, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
@ -970,7 +986,7 @@ class LogViewer(QDialog): # {{{
# }}} # }}}
class FullFetch(QDialog): # {{{ class FullFetch(QDialog): # {{{
def __init__(self, current_cover=None, parent=None): def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
@ -1085,7 +1101,7 @@ class FullFetch(QDialog): # {{{
return self.exec_() return self.exec_()
# }}} # }}}
class CoverFetch(QDialog): # {{{ class CoverFetch(QDialog): # {{{
def __init__(self, current_cover=None, parent=None): def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)

View File

@ -129,7 +129,7 @@
<item row="6" column="0"> <item row="6" column="0">
<widget class="QLabel" name="label_16"> <widget class="QLabel" name="label_16">
<property name="text"> <property name="text">
<string>Max. OPDS &amp;ungrouped items:</string> <string>Max. &amp;ungrouped items:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>opt_max_opds_ungrouped_items</cstring> <cstring>opt_max_opds_ungrouped_items</cstring>

View File

@ -15,16 +15,17 @@ from calibre.gui2 import choose_save_file, gprefs
class ImageView(QDialog): class ImageView(QDialog):
def __init__(self, parent, current_img, current_url): def __init__(self, parent, current_img, current_url, geom_name='viewer_image_popup_geometry'):
QDialog.__init__(self) QDialog.__init__(self)
dw = QApplication.instance().desktop() dw = QApplication.instance().desktop()
self.avail_geom = dw.availableGeometry(parent) self.avail_geom = dw.availableGeometry(parent)
self.current_img = current_img self.current_img = current_img
self.current_url = current_url self.current_url = current_url
self.factor = 1.0 self.factor = 1.0
self.geom_name = geom_name
self.label = l = QLabel() self.label = l = QLabel()
l.setBackgroundRole(QPalette.Base); l.setBackgroundRole(QPalette.Base)
l.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) l.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
l.setScaledContents(True) l.setScaledContents(True)
@ -88,21 +89,27 @@ class ImageView(QDialog):
self.label.setPixmap(pm) self.label.setPixmap(pm)
self.label.adjustSize() self.label.adjustSize()
def __call__(self): def __call__(self, use_exec=False):
geom = self.avail_geom geom = self.avail_geom
self.label.setPixmap(self.current_img) self.label.setPixmap(self.current_img)
self.label.adjustSize() self.label.adjustSize()
self.resize(QSize(int(geom.width()/2.5), geom.height()-50)) self.resize(QSize(int(geom.width()/2.5), geom.height()-50))
geom = gprefs.get('viewer_image_popup_geometry', None) geom = gprefs.get(self.geom_name, None)
if geom is not None: if geom is not None:
self.restoreGeometry(geom) self.restoreGeometry(geom)
self.current_image_name = unicode(self.current_url.toString()).rpartition('/')[-1] try:
self.current_image_name = unicode(self.current_url.toString()).rpartition('/')[-1]
except AttributeError:
self.current_image_name = self.current_url
title = _('View Image: %s')%self.current_image_name title = _('View Image: %s')%self.current_image_name
self.setWindowTitle(title) self.setWindowTitle(title)
self.show() if use_exec:
self.exec_()
else:
self.show()
def done(self, e): def done(self, e):
gprefs['viewer_image_popup_geometry'] = bytearray(self.saveGeometry()) gprefs[self.geom_name] = bytearray(self.saveGeometry())
return QDialog.done(self, e) return QDialog.done(self, e)
def wheelEvent(self, event): def wheelEvent(self, event):