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
d18c934e32
@ -114,6 +114,19 @@ class Cache(object):
|
||||
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
|
||||
|
@ -6,12 +6,13 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
__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 @@ class LibraryDatabase(object):
|
||||
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 @@ class LibraryDatabase(object):
|
||||
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 @@ class LibraryDatabase(object):
|
||||
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 @@ class LibraryDatabase(object):
|
||||
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
|
||||
|
||||
|
@ -16,7 +16,7 @@ class LegacyTest(BaseTest):
|
||||
'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 @@ class LegacyTest(BaseTest):
|
||||
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 @@ 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)
|
||||
# }}}
|
||||
|
||||
|
@ -294,3 +294,11 @@ class View(object):
|
||||
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)
|
||||
|
||||
|
@ -1199,9 +1199,9 @@ class KOBO(USBMS):
|
||||
|
||||
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 @@ class KOBOTOUCH(KOBO):
|
||||
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 @@ class KOBOTOUCH(KOBO):
|
||||
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 @@ class KOBOTOUCH(KOBO):
|
||||
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 @@ class KOBOTOUCH(KOBO):
|
||||
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 @@ class KOBOTOUCH(KOBO):
|
||||
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 @@ class KOBOTOUCH(KOBO):
|
||||
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 @@ class KOBOTOUCH(KOBO):
|
||||
# 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):
|
||||
|
@ -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 @@ class WAYTEQ(USBMS):
|
||||
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 @@ class WAYTEQ(USBMS):
|
||||
|
||||
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 @@ class WAYTEQ(USBMS):
|
||||
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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -188,7 +188,6 @@ class EPUBInput(InputFormatPlugin):
|
||||
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():
|
||||
|
@ -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 @@ import os, re, uuid, logging
|
||||
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 @@ CALIBRE_NS = 'http://calibre.kovidgoyal.net/2009/metadata'
|
||||
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 @@ URL_SAFE = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
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 @@ class DirContainer(object):
|
||||
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 @@ class DirContainer(object):
|
||||
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 @@ class Metadata(object):
|
||||
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 @@ class Metadata(object):
|
||||
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 @@ class Manifest(object):
|
||||
|
||||
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 @@ class Manifest(object):
|
||||
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 @@ class Manifest(object):
|
||||
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:]
|
||||
|
@ -148,7 +148,6 @@ class OEBReader(object):
|
||||
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 @@ class OEBReader(object):
|
||||
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 @@ class OEBReader(object):
|
||||
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 @@ class OEBReader(object):
|
||||
|
||||
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 @@ class OEBReader(object):
|
||||
method='text', encoding=unicode).strip()
|
||||
if not description:
|
||||
description = None
|
||||
else :
|
||||
else:
|
||||
description = None
|
||||
|
||||
index_image = xpath(child,
|
||||
@ -497,7 +498,8 @@ class OEBReader(object):
|
||||
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 @@ class OEBReader(object):
|
||||
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 @@ class OEBReader(object):
|
||||
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):
|
||||
|
@ -47,6 +47,8 @@ class ManifestTrimmer(object):
|
||||
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:
|
||||
|
@ -21,7 +21,7 @@ from PyQt4.Qt import (
|
||||
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.utils.ipc.simple_worker import fork_job, WorkerError
|
||||
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 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||
painter.restore()
|
||||
# }}}
|
||||
|
||||
class CoverDelegate(QStyledItemDelegate): # {{{
|
||||
class CoverDelegate(QStyledItemDelegate): # {{{
|
||||
|
||||
needs_redraw = pyqtSignal()
|
||||
|
||||
@ -143,7 +143,7 @@ class CoverDelegate(QStyledItemDelegate): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class ResultsModel(QAbstractTableModel): # {{{
|
||||
class ResultsModel(QAbstractTableModel): # {{{
|
||||
|
||||
COLUMNS = (
|
||||
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
|
||||
@ -182,7 +182,6 @@ class ResultsModel(QAbstractTableModel): # {{{
|
||||
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 @@ class ResultsModel(QAbstractTableModel): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class ResultsView(QTableView): # {{{
|
||||
class ResultsView(QTableView): # {{{
|
||||
|
||||
show_details_signal = pyqtSignal(object)
|
||||
book_selected = pyqtSignal(object)
|
||||
@ -316,7 +315,7 @@ class ResultsView(QTableView): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class Comments(QWebView): # {{{
|
||||
class Comments(QWebView): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWebView.__init__(self, parent)
|
||||
@ -384,7 +383,7 @@ class Comments(QWebView): # {{{
|
||||
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 @@ class IdentifyWorker(Thread): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class IdentifyWidget(QWidget): # {{{
|
||||
class IdentifyWidget(QWidget): # {{{
|
||||
|
||||
rejected = pyqtSignal()
|
||||
results_found = pyqtSignal()
|
||||
@ -552,12 +551,11 @@ class IdentifyWidget(QWidget): # {{{
|
||||
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 @@ class CoverWorker(Thread): # {{{
|
||||
|
||||
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 @@ class CoverWorker(Thread): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class CoversModel(QAbstractListModel): # {{{
|
||||
class CoversModel(QAbstractListModel): # {{{
|
||||
|
||||
def __init__(self, current_cover, parent=None):
|
||||
QAbstractListModel.__init__(self, parent)
|
||||
@ -770,7 +769,7 @@ class CoversModel(QAbstractListModel): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class CoversView(QListView): # {{{
|
||||
class CoversView(QListView): # {{{
|
||||
|
||||
chosen = pyqtSignal()
|
||||
|
||||
@ -793,6 +792,8 @@ class CoversView(QListView): # {{{
|
||||
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 @@ class CoversView(QListView): # {{{
|
||||
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 @@ class CoversWidget(QWidget): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class LogViewer(QDialog): # {{{
|
||||
class LogViewer(QDialog): # {{{
|
||||
|
||||
def __init__(self, log, parent=None):
|
||||
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):
|
||||
QDialog.__init__(self, parent)
|
||||
@ -1085,7 +1101,7 @@ class FullFetch(QDialog): # {{{
|
||||
return self.exec_()
|
||||
# }}}
|
||||
|
||||
class CoverFetch(QDialog): # {{{
|
||||
class CoverFetch(QDialog): # {{{
|
||||
|
||||
def __init__(self, current_cover=None, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
|
@ -129,7 +129,7 @@
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="text">
|
||||
<string>Max. OPDS &ungrouped items:</string>
|
||||
<string>Max. &ungrouped items:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_max_opds_ungrouped_items</cstring>
|
||||
|
@ -15,16 +15,17 @@ from calibre.gui2 import choose_save_file, gprefs
|
||||
|
||||
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 @@ class ImageView(QDialog):
|
||||
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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user