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 @@ def initialize_dynamic(self):
if self.dirtied_cache:
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
def field_metadata(self):
return self.backend.field_metadata

View file

@ -6,12 +6,13 @@
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import os
import os, traceback
from functools import partial
from calibre.db.backend import DB
from calibre.db.cache import Cache
from calibre.db.view import View
from calibre.utils.date import utcnow
class LibraryDatabase(object):
@ -29,6 +30,7 @@ def __init__(self, library_path,
progress_callback=lambda x, y:True, restore_all_prefs=False):
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,
read_only=read_only, restore_all_prefs=restore_all_prefs,
@ -50,6 +52,8 @@ def __init__(self, library_path,
setattr(self, prop, partial(self.get_property,
loc=self.FIELD_MAP[fm]))
self.last_update_check = self.last_modified()
def close(self):
self.backend.close()
@ -71,9 +75,22 @@ def user_version(self):
def library_id(self):
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):
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
def custom_column_num_map(self):
return self.backend.custom_column_num_map
@ -86,9 +103,48 @@ def custom_column_label_map(self):
def FIELD_MAP(self):
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):
for book_id in self.data.cache.all_book_ids():
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 @@ def test_library_wide_properties(self): # {{{
'Test library wide properties'
def get_props(db):
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', )
ans = {x:getattr(db, x) for x in props}
ans.update({x:getattr(db, x)() for x in fprops})
@ -51,6 +51,11 @@ def get_values(db):
if label in {'tags', 'formats'}:
# Order is random in the old db for these
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
old = self.init_old()
@ -64,3 +69,31 @@ def get_values(db):
# }}}
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 @@ def set_marked_ids(self, id_dict):
self.marked_ids = dict(izip(id_dict.iterkeys(), imap(unicode,
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 @@ def add_annotation_to_library(self, db, db_id, annotation):
class KOBOTOUCH(KOBO):
name = 'KoboTouch'
gui_name = 'Kobo Touch'
gui_name = 'Kobo Touch/Glo/Mini/Aura HD'
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')
supported_dbversion = 80
@ -1297,12 +1297,13 @@ class KOBOTOUCH(KOBO):
TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ"
GLO_PRODUCT_ID = [0x4173]
MINI_PRODUCT_ID = [0x4183]
TOUCH_PRODUCT_ID = [0x4163]
PRODUCT_ID = GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID
AURA_HD_PRODUCT_ID = [0x4193]
GLO_PRODUCT_ID = [0x4173]
MINI_PRODUCT_ID = [0x4183]
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,
COVER_FILE_ENDINGS = {
@ -1319,6 +1320,11 @@ class KOBOTOUCH(KOBO):
# ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,],
# ' - 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
# COVER_FILE_ENDINGS = {
# ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen
@ -1334,6 +1340,10 @@ def initialize(self):
super(KOBOTOUCH, self).initialize()
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):
debug_print("KoboTouch:books - oncard='%s'"%oncard)
from calibre.ebooks.metadata.meta import path_to_ext
@ -1366,7 +1376,7 @@ def books(self, oncard=None, end_session=True):
except:
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 firmware:', self.fwversion, 'Has kepubs:', self.has_kepubs)
@ -1379,7 +1389,7 @@ def books(self, oncard=None, end_session=True):
debug_print(opts.extra_customization)
if opts.extra_customization:
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)
debug_print("KoboTouch:books - length bl=%d"%len(bl))
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)
@ -1930,7 +1940,7 @@ def update_device_database_collections(self, booklists, collections_attributes,
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()
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)
else:
delete_empty_shelves = False
@ -2516,6 +2526,8 @@ def settings(cls):
return opts
def isAuraHD(self):
return self.detected_device.idProduct in self.AURA_HD_PRODUCT_ID
def isGlo(self):
return self.detected_device.idProduct in self.GLO_PRODUCT_ID
def isMini(self):
@ -2524,7 +2536,21 @@ def isTouch(self):
return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID
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):
opts = self.settings()
@ -2582,14 +2608,6 @@ def modify_database_check(self, function):
# Supported database version
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
def is_debugging_title(cls, title):

View file

@ -95,7 +95,6 @@ class PDNOVEL(USBMS):
SUPPORTS_SUB_DIRS = False
DELETE_EXTS = ['.jpg', '.jpeg', '.png']
def upload_cover(self, path, filename, metadata, filepath):
coverdata = getattr(metadata, 'thumbnail', None)
if coverdata and coverdata[2]:
@ -226,9 +225,9 @@ class TREKSTOR(USBMS):
VENDOR_ID = [0x1e68]
PRODUCT_ID = [0x0041, 0x0042, 0x0052, 0x004e, 0x0056,
0x0067, # This is for the Pyrus Mini
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
0x0067, # This is for the Pyrus Mini
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
]
BCD = [0x0002, 0x100]
@ -427,8 +426,8 @@ class WAYTEQ(USBMS):
EBOOK_DIR_MAIN = 'Documents'
SCAN_FROM_ROOT = True
VENDOR_NAME = 'ROCKCHIP'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'RK28_SDK_DEMO'
VENDOR_NAME = ['ROCKCHIP', 'CBR']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['RK28_SDK_DEMO', 'EINK_EBOOK_READE']
SUPPORTS_SUB_DIRS = True
def get_gui_name(self):
@ -445,7 +444,8 @@ def get_carda_ebook_dir(self, for_upload=False):
return self.EBOOK_DIR_CARD_A
def windows_sort_drives(self, drives):
if len(drives) < 2: return drives
if len(drives) < 2:
return drives
main = drives.get('main', None)
carda = drives.get('carda', None)
if main and carda:
@ -455,7 +455,8 @@ def windows_sort_drives(self, drives):
def linux_swap_drives(self, drives):
# 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)
t = drives[0]
drives[0] = drives[1]
@ -463,7 +464,8 @@ def linux_swap_drives(self, drives):
return tuple(drives)
def osx_sort_names(self, names):
if len(names) < 2: return names
if len(names) < 2:
return names
main = names.get('main', None)
card = names.get('carda', None)

View file

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

View file

@ -188,7 +188,6 @@ def convert(self, stream, options, file_ext, log, accelerators):
raise DRMError(os.path.basename(path))
self.encrypted_fonts = self._encrypted_font_uris
if len(parts) > 1 and parts[0]:
delta = '/'.join(parts[:-1])+'/'
for elem in opf.itermanifest():

View file

@ -1,7 +1,6 @@
'''
Basic support for manipulating OEB 1.x/2.0 content and metadata.
'''
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
@ -11,7 +10,7 @@
from collections import defaultdict
from itertools import count
from urlparse import urldefrag, urlparse, urlunparse, urljoin
from urllib import unquote as urlunquote
from urllib import unquote
from lxml import etree, html
from calibre.constants import filesystem_encoding, __version__
@ -40,11 +39,11 @@
RE_NS = 'http://exslt.org/regular-expressions'
MBP_NS = 'http://www.mobipocket.com'
XPNSMAP = {'h' : XHTML_NS, 'o1' : OPF1_NS, 'o2' : OPF2_NS,
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
'xsi': XSI_NS, 'dt' : DCTERMS_NS, 'ncx': NCX_NS,
'svg': SVG_NS, 'xl' : XLINK_NS, 're': RE_NS,
'mbp': MBP_NS, 'calibre': CALIBRE_NS }
XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS,
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS,
'svg': SVG_NS, 'xl': XLINK_NS, 're': RE_NS,
'mbp': MBP_NS, 'calibre': CALIBRE_NS}
OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_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:
yield (el, attr, attribs[attr], 0)
if not find_links_in_css:
continue
if tag == XHTML('style') and el.text:
@ -363,7 +361,9 @@ def serialize(data, media_type, pretty_print=False):
URL_UNSAFE = [ASCII_CHARS - URL_SAFE, UNIBYTE_CHARS - URL_SAFE]
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 = []
unsafe = 0 if isinstance(href, unicode) else 1
unsafe = URL_UNSAFE[unsafe]
@ -373,6 +373,19 @@ def urlquote(href):
result.append(char)
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):
"""Convert a URL into normalized form, with all and only URL-unsafe
characters URL quoted.
@ -469,7 +482,7 @@ def __init__(self, path, log, ignore_opf=False):
return
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
# string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8')
# and the latter is correct
@ -497,7 +510,7 @@ def exists(self, path):
return False
try:
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
try:
return os.path.isfile(path)
@ -577,12 +590,13 @@ def term_attr(self, obj):
allowed = self.allowed
if allowed is not None and term not in allowed:
raise AttributeError(
'attribute %r not valid for metadata term %r' \
'attribute %r not valid for metadata term %r'
% (self.attr(term), barename(obj.term)))
return self.attr(term)
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), '')
def __set__(self, obj, value):
@ -628,8 +642,8 @@ def fset(self, value):
self.value = value
return property(fget=fget, fset=fset)
scheme = Attribute(lambda term: 'scheme' if \
term == OPF('meta') else OPF('scheme'),
scheme = Attribute(lambda term: 'scheme' if
term == OPF('meta') else OPF('scheme'),
[DC('identifier'), OPF('meta')])
file_as = Attribute(OPF('file-as'), [DC('creator'), DC('contributor'),
DC('title')])
@ -882,7 +896,6 @@ def _parse_txt(self, data):
return self._parse_xhtml(convert_markdown(data, title=title))
def _parse_css(self, data):
from cssutils import CSSParser, log, resolveImports
log.setLevel(logging.WARN)
@ -935,7 +948,7 @@ def fget(self):
data = self._loader(getattr(self, 'html_input_href',
self.href))
if not isinstance(data, basestring):
pass # already parsed
pass # already parsed
elif self.media_type.lower() in OEB_DOCS:
data = self._parse_xhtml(data)
elif self.media_type.lower()[-4:] in ('+xml', '/xml'):
@ -1022,7 +1035,8 @@ def relhref(self, href):
target, frag = urldefrag(href)
target = target.split('/')
for index in xrange(min(len(base), len(target))):
if base[index] != target[index]: break
if base[index] != target[index]:
break
else:
index += 1
relhref = (['..'] * (len(base) - index)) + target[index:]

View file

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

View file

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

View file

@ -21,7 +21,7 @@
QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget,
QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal,
QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex,
QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel)
QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel, QMenu, QCursor)
from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins
@ -40,7 +40,7 @@
from calibre.ptempfile import TemporaryDirectory
# }}}
class RichTextDelegate(QStyledItemDelegate): # {{{
class RichTextDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent=None, max_width=160):
QStyledItemDelegate.__init__(self, parent)
@ -77,7 +77,7 @@ def paint(self, painter, option, index):
painter.restore()
# }}}
class CoverDelegate(QStyledItemDelegate): # {{{
class CoverDelegate(QStyledItemDelegate): # {{{
needs_redraw = pyqtSignal()
@ -143,7 +143,7 @@ def paint(self, painter, option, index):
# }}}
class ResultsModel(QAbstractTableModel): # {{{
class ResultsModel(QAbstractTableModel): # {{{
COLUMNS = (
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
@ -182,7 +182,6 @@ def data_as_text(self, book, col):
p = book.publisher if book.publisher else ''
return '<b>%s</b><br><i>%s</i>' % (d, p)
def data(self, index, role):
row, col = index.row(), index.column()
try:
@ -233,7 +232,7 @@ def dategetter(x):
# }}}
class ResultsView(QTableView): # {{{
class ResultsView(QTableView): # {{{
show_details_signal = pyqtSignal(object)
book_selected = pyqtSignal(object)
@ -316,7 +315,7 @@ def get_result(self):
# }}}
class Comments(QWebView): # {{{
class Comments(QWebView): # {{{
def __init__(self, parent=None):
QWebView.__init__(self, parent)
@ -384,7 +383,7 @@ def sizeHint(self):
return QSize(800, 300)
# }}}
class IdentifyWorker(Thread): # {{{
class IdentifyWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers, caches):
Thread.__init__(self)
@ -441,7 +440,7 @@ def run(self):
# }}}
class IdentifyWidget(QWidget): # {{{
class IdentifyWidget(QWidget): # {{{
rejected = pyqtSignal()
results_found = pyqtSignal()
@ -552,12 +551,11 @@ def process_results(self):
self.results_view.show_results(self.worker.results)
self.results_found.emit()
def cancel(self):
self.abort.set()
# }}}
class CoverWorker(Thread): # {{{
class CoverWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers, caches):
Thread.__init__(self)
@ -609,7 +607,8 @@ def run_fork(self):
def scan_once(self, tdir, seen):
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,
x+'.done')):
name = x.rpartition('.')[0]
@ -635,7 +634,7 @@ def monitor_tdir(self, tdir):
# }}}
class CoversModel(QAbstractListModel): # {{{
class CoversModel(QAbstractListModel): # {{{
def __init__(self, current_cover, parent=None):
QAbstractListModel.__init__(self, parent)
@ -770,7 +769,7 @@ def cover_pixmap(self, index):
# }}}
class CoversView(QListView): # {{{
class CoversView(QListView): # {{{
chosen = pyqtSignal()
@ -793,6 +792,8 @@ def __init__(self, current_cover, parent=None):
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):
current = self.model().index(num)
@ -814,9 +815,24 @@ def clear_failed(self):
else:
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()
finished = pyqtSignal()
@ -922,7 +938,7 @@ def cover_pixmap(self):
# }}}
class LogViewer(QDialog): # {{{
class LogViewer(QDialog): # {{{
def __init__(self, log, parent=None):
QDialog.__init__(self, parent)
@ -970,7 +986,7 @@ def update_log(self):
# }}}
class FullFetch(QDialog): # {{{
class FullFetch(QDialog): # {{{
def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent)
@ -1085,7 +1101,7 @@ def start(self, title=None, authors=None, identifiers={}):
return self.exec_()
# }}}
class CoverFetch(QDialog): # {{{
class CoverFetch(QDialog): # {{{
def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent)

View file

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

View file

@ -15,16 +15,17 @@
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)
dw = QApplication.instance().desktop()
self.avail_geom = dw.availableGeometry(parent)
self.current_img = current_img
self.current_url = current_url
self.factor = 1.0
self.geom_name = geom_name
self.label = l = QLabel()
l.setBackgroundRole(QPalette.Base);
l.setBackgroundRole(QPalette.Base)
l.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
l.setScaledContents(True)
@ -88,21 +89,27 @@ def rotate_image(self):
self.label.setPixmap(pm)
self.label.adjustSize()
def __call__(self):
def __call__(self, use_exec=False):
geom = self.avail_geom
self.label.setPixmap(self.current_img)
self.label.adjustSize()
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:
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
self.setWindowTitle(title)
self.show()
if use_exec:
self.exec_()
else:
self.show()
def done(self, e):
gprefs['viewer_image_popup_geometry'] = bytearray(self.saveGeometry())
gprefs[self.geom_name] = bytearray(self.saveGeometry())
return QDialog.done(self, e)
def wheelEvent(self, event):