mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
GwR revisions for fetch annotations
This commit is contained in:
commit
a14fdbc543
@ -129,18 +129,18 @@ class Plugin(object):
|
|||||||
zip_safe = False
|
zip_safe = False
|
||||||
if zip_safe:
|
if zip_safe:
|
||||||
sys.path.insert(0, self.plugin_path)
|
sys.path.insert(0, self.plugin_path)
|
||||||
self._sys_insertion_path = self.plugin_path
|
self.sys_insertion_path = self.plugin_path
|
||||||
else:
|
else:
|
||||||
from calibre.ptempfile import TemporaryDirectory
|
from calibre.ptempfile import TemporaryDirectory
|
||||||
self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip')
|
self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip')
|
||||||
self._sys_insertion_path = self._sys_insertion_tdir.__enter__(*args)
|
self.sys_insertion_path = self._sys_insertion_tdir.__enter__(*args)
|
||||||
zf.extractall(self._sys_insertion_path)
|
zf.extractall(self.sys_insertion_path)
|
||||||
sys.path.insert(0, self._sys_insertion_path)
|
sys.path.insert(0, self.sys_insertion_path)
|
||||||
zf.close()
|
zf.close()
|
||||||
|
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
ip, it = getattr(self, '_sys_insertion_path', None), getattr(self,
|
ip, it = getattr(self, 'sys_insertion_path', None), getattr(self,
|
||||||
'_sys_insertion_tdir', None)
|
'_sys_insertion_tdir', None)
|
||||||
if ip in sys.path:
|
if ip in sys.path:
|
||||||
sys.path.remove(ip)
|
sys.path.remove(ip)
|
||||||
|
@ -8,11 +8,14 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Device driver for Amazon's Kindle
|
Device driver for Amazon's Kindle
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
from cStringIO import StringIO
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from struct import unpack
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from calibre.devices.usbms.driver import USBMS
|
from calibre.devices.usbms.driver import USBMS
|
||||||
|
from calibre.ebooks.metadata.mobi import StreamSlicer
|
||||||
|
|
||||||
class KINDLE(USBMS):
|
class KINDLE(USBMS):
|
||||||
|
|
||||||
@ -44,6 +47,7 @@ class KINDLE(USBMS):
|
|||||||
EBOOK_DIR_CARD_A = 'documents'
|
EBOOK_DIR_CARD_A = 'documents'
|
||||||
DELETE_EXTS = ['.mbp']
|
DELETE_EXTS = ['.mbp']
|
||||||
SUPPORTS_SUB_DIRS = True
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
SUPPORTS_ANNOTATIONS = True
|
||||||
|
|
||||||
WIRELESS_FILE_NAME_PATTERN = re.compile(
|
WIRELESS_FILE_NAME_PATTERN = re.compile(
|
||||||
r'(?P<title>[^-]+)-asin_(?P<asin>[a-zA-Z\d]{10,})-type_(?P<type>\w{4})-v_(?P<index>\d+).*')
|
r'(?P<title>[^-]+)-asin_(?P<asin>[a-zA-Z\d]{10,})-type_(?P<type>\w{4})-v_(?P<index>\d+).*')
|
||||||
@ -60,6 +64,51 @@ class KINDLE(USBMS):
|
|||||||
'replace')
|
'replace')
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
|
def get_annotations(self, path_map):
|
||||||
|
def get_storage():
|
||||||
|
storage = []
|
||||||
|
if self._main_prefix:
|
||||||
|
storage.append(os.path.join(self._main_prefix, self.EBOOK_DIR_MAIN))
|
||||||
|
if self._card_a_prefix:
|
||||||
|
storage.append(os.path.join(self._card_a_prefix, self.EBOOK_DIR_CARD_A))
|
||||||
|
if self._card_b_prefix:
|
||||||
|
storage.append(os.path.join(self._card_b_prefix, self.EBOOK_DIR_CARD_B))
|
||||||
|
return storage
|
||||||
|
|
||||||
|
def resolve_mbp_paths(storage, path_map):
|
||||||
|
pop_list = []
|
||||||
|
for id in path_map:
|
||||||
|
for vol in storage:
|
||||||
|
#print "path_map[id]: %s" % path_map[id]
|
||||||
|
mbp_path = path_map[id].replace(os.path.abspath('/<storage>'),vol)
|
||||||
|
#print "looking for mbp_path: %s" % mbp_path
|
||||||
|
if os.path.exists(mbp_path):
|
||||||
|
#print "mbp_path found"
|
||||||
|
path_map[id] = mbp_path
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
#print "mbp_path not found"
|
||||||
|
pop_list.append(id)
|
||||||
|
|
||||||
|
# Remove non-existent mbp files
|
||||||
|
for id in pop_list:
|
||||||
|
path_map.pop(id)
|
||||||
|
return path_map
|
||||||
|
|
||||||
|
storage = get_storage()
|
||||||
|
path_map = resolve_mbp_paths(storage, path_map)
|
||||||
|
|
||||||
|
# path_map is now a mapping of valid mbp files
|
||||||
|
# Not yet implemented - Topaz annotations
|
||||||
|
bookmarked_books = {}
|
||||||
|
MBP_FORMATS = ['azw', 'mobi', 'prc', 'txt']
|
||||||
|
for id in path_map:
|
||||||
|
myBookmark = Bookmark(path_map[id], MBP_FORMATS, id)
|
||||||
|
bookmarked_books[id] = self.UserAnnotation(type='mobi', bookmark=myBookmark)
|
||||||
|
|
||||||
|
# This returns as job.result in gui2.ui.annotations_fetched(self,job)
|
||||||
|
return bookmarked_books
|
||||||
|
|
||||||
|
|
||||||
class KINDLE2(KINDLE):
|
class KINDLE2(KINDLE):
|
||||||
|
|
||||||
@ -79,3 +128,139 @@ class KINDLE_DX(KINDLE2):
|
|||||||
|
|
||||||
PRODUCT_ID = [0x0003]
|
PRODUCT_ID = [0x0003]
|
||||||
BCD = [0x0100]
|
BCD = [0x0100]
|
||||||
|
|
||||||
|
class Bookmark():
|
||||||
|
'''
|
||||||
|
A simple class fetching bookmark data
|
||||||
|
Kindle-specific
|
||||||
|
'''
|
||||||
|
def __init__(self, path, formats, id):
|
||||||
|
self.book_format = None
|
||||||
|
self.book_length = 0
|
||||||
|
self.id = id
|
||||||
|
self.last_read_location = 0
|
||||||
|
self.timestamp = 0
|
||||||
|
self.user_notes = None
|
||||||
|
|
||||||
|
self.get_bookmark_data(path)
|
||||||
|
self.get_book_length(path, formats)
|
||||||
|
try:
|
||||||
|
self.percent_read = float(100*self.last_read_location / self.book_length)
|
||||||
|
except:
|
||||||
|
self.percent_read = 0
|
||||||
|
|
||||||
|
def record(self, n):
|
||||||
|
if n >= self.nrecs:
|
||||||
|
raise ValueError('non-existent record %r' % n)
|
||||||
|
offoff = 78 + (8 * n)
|
||||||
|
start, = unpack('>I', self.data[offoff + 0:offoff + 4])
|
||||||
|
stop = None
|
||||||
|
if n < (self.nrecs - 1):
|
||||||
|
stop, = unpack('>I', self.data[offoff + 8:offoff + 12])
|
||||||
|
return StreamSlicer(self.stream, start, stop)
|
||||||
|
|
||||||
|
def get_bookmark_data(self, path, fetchUserNotes=True):
|
||||||
|
''' Return the timestamp and last_read_location '''
|
||||||
|
with open(path,'rb') as f:
|
||||||
|
stream = StringIO(f.read())
|
||||||
|
data = StreamSlicer(stream)
|
||||||
|
self.timestamp, = unpack('>I', data[0x24:0x28])
|
||||||
|
bpar_offset, = unpack('>I', data[0x4e:0x52])
|
||||||
|
lrlo = bpar_offset + 0x0c
|
||||||
|
self.last_read_location = int(unpack('>I', data[lrlo:lrlo+4])[0])
|
||||||
|
entries, = unpack('>I', data[0x4a:0x4e])
|
||||||
|
|
||||||
|
# Store the annotations/locations
|
||||||
|
if fetchUserNotes:
|
||||||
|
bpl = bpar_offset + 4
|
||||||
|
bpar_len, = unpack('>I', data[bpl:bpl+4])
|
||||||
|
bpar_len += 8
|
||||||
|
#print "bpar_len: 0x%x" % bpar_len
|
||||||
|
eo = bpar_offset + bpar_len
|
||||||
|
|
||||||
|
# Walk bookmark entries
|
||||||
|
#print " --- %s --- " % path
|
||||||
|
#print " last_read_location: %d" % self.magicKindleLocationCalculator(last_read_location)
|
||||||
|
current_entry = 1
|
||||||
|
sig = data[eo:eo+4]
|
||||||
|
previous_block = None
|
||||||
|
user_notes = {}
|
||||||
|
|
||||||
|
while sig == 'DATA':
|
||||||
|
text = None
|
||||||
|
entry_type = None
|
||||||
|
rec_len, = unpack('>I', data[eo+4:eo+8])
|
||||||
|
if rec_len == 0:
|
||||||
|
current_block = "empty_data"
|
||||||
|
elif data[eo+8:eo+12] == "EBAR":
|
||||||
|
current_block = "data_header"
|
||||||
|
#entry_type = "data_header"
|
||||||
|
location, = unpack('>I', data[eo+0x34:eo+0x38])
|
||||||
|
#print "data_header location: %d" % location
|
||||||
|
else:
|
||||||
|
current_block = "text_block"
|
||||||
|
if previous_block == 'empty_data':
|
||||||
|
entry_type = 'Note'
|
||||||
|
elif previous_block == 'data_header':
|
||||||
|
entry_type = 'Highlight'
|
||||||
|
text = data[eo+8:eo+8+rec_len].decode('utf-16-be')
|
||||||
|
|
||||||
|
if entry_type:
|
||||||
|
user_notes[location] = dict(type=entry_type, id=self.id,
|
||||||
|
text=data[eo+8:eo+8+rec_len].decode('utf-16-be'))
|
||||||
|
#print " %2d: %s %s" % (current_entry, entry_type,'at %d' % location if location else '')
|
||||||
|
#if current_block == 'text_block':
|
||||||
|
#self.textdump(text)
|
||||||
|
|
||||||
|
eo += rec_len + 8
|
||||||
|
current_entry += 1
|
||||||
|
previous_block = current_block
|
||||||
|
sig = data[eo:eo+4]
|
||||||
|
|
||||||
|
while sig == 'BKMK':
|
||||||
|
# Fix start location for Highlights using BKMK data
|
||||||
|
end_loc, = unpack('>I', data[eo+0x10:eo+0x14])
|
||||||
|
if end_loc in user_notes and user_notes[end_loc]['type'] != 'Note':
|
||||||
|
start, = unpack('>I', data[eo+8:eo+12])
|
||||||
|
user_notes[start] = user_notes[end_loc]
|
||||||
|
user_notes.pop(end_loc)
|
||||||
|
#print "changing start location of %d to %d" % (end_loc,start)
|
||||||
|
else:
|
||||||
|
# If a bookmark coincides with a user annotation, the locs could
|
||||||
|
# be the same - cheat by nudging -1
|
||||||
|
# Skip bookmark for last_read_location
|
||||||
|
if end_loc != self.last_read_location:
|
||||||
|
user_notes[end_loc - 1] = dict(type='Bookmark',id=self.id,text=None)
|
||||||
|
rec_len, = unpack('>I', data[eo+4:eo+8])
|
||||||
|
eo += rec_len + 8
|
||||||
|
sig = data[eo:eo+4]
|
||||||
|
|
||||||
|
'''
|
||||||
|
for location in sorted(user_notes):
|
||||||
|
print ' Location %d: %s\n%s' % self.magicKindleLocationCalculator(location),
|
||||||
|
user_notes[location]['type'],
|
||||||
|
'\n'.join(self.textdump(user_notes[location]['text'])))
|
||||||
|
'''
|
||||||
|
self.user_notes = user_notes
|
||||||
|
|
||||||
|
def get_book_length(self, path, formats):
|
||||||
|
# This assumes only one of the possible formats exists on the Kindle
|
||||||
|
book_fs = None
|
||||||
|
for format in formats:
|
||||||
|
fmt = format.rpartition('.')[2]
|
||||||
|
book_fs = path.replace('.mbp','.%s' % fmt)
|
||||||
|
if os.path.exists(book_fs):
|
||||||
|
self.book_format = fmt
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
#print "no files matching library formats exist on device"
|
||||||
|
self.book_length = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read the book len from the header
|
||||||
|
with open(book_fs,'rb') as f:
|
||||||
|
self.stream = StringIO(f.read())
|
||||||
|
self.data = StreamSlicer(self.stream)
|
||||||
|
self.nrecs, = unpack('>H', self.data[76:78])
|
||||||
|
record0 = self.record(0)
|
||||||
|
self.book_length = int(unpack('>I', record0[0x04:0x08])[0])
|
||||||
|
@ -17,6 +17,8 @@ import time
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
|
|
||||||
from calibre.devices.interface import DevicePlugin
|
from calibre.devices.interface import DevicePlugin
|
||||||
@ -88,6 +90,8 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
EBOOK_DIR_CARD_B = ''
|
EBOOK_DIR_CARD_B = ''
|
||||||
DELETE_EXTS = []
|
DELETE_EXTS = []
|
||||||
|
|
||||||
|
# Used by gui2.ui:annotations_fetched() and devices.kindle.driver:get_annotations()
|
||||||
|
UserAnnotation = namedtuple('Annotation','type, bookmark')
|
||||||
|
|
||||||
def reset(self, key='-1', log_packets=False, report_progress=None,
|
def reset(self, key='-1', log_packets=False, report_progress=None,
|
||||||
detected_device=None):
|
detected_device=None):
|
||||||
@ -793,6 +797,12 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
'''
|
'''
|
||||||
return components
|
return components
|
||||||
|
|
||||||
|
def get_annotations(self, path_map):
|
||||||
|
'''
|
||||||
|
Resolve path_map to annotation_map of files found on the device
|
||||||
|
'''
|
||||||
|
return {}
|
||||||
|
|
||||||
def create_upload_path(self, path, mdata, fname, create_dirs=True):
|
def create_upload_path(self, path, mdata, fname, create_dirs=True):
|
||||||
path = os.path.abspath(path)
|
path = os.path.abspath(path)
|
||||||
extra_components = []
|
extra_components = []
|
||||||
|
@ -123,7 +123,7 @@ class USBMS(CLI, Device):
|
|||||||
'''
|
'''
|
||||||
:path: the full path were the associated book is located.
|
:path: the full path were the associated book is located.
|
||||||
:filename: the name of the book file without the extension.
|
:filename: the name of the book file without the extension.
|
||||||
:metatdata: metadata belonging to the book. Use metadata.thumbnail
|
:metadata: metadata belonging to the book. Use metadata.thumbnail
|
||||||
for cover
|
for cover
|
||||||
'''
|
'''
|
||||||
pass
|
pass
|
||||||
|
@ -79,7 +79,7 @@ class PML_HTMLizer(object):
|
|||||||
'd': ('<span style="text-decoration: line-through;">', '</span>'),
|
'd': ('<span style="text-decoration: line-through;">', '</span>'),
|
||||||
'b': ('<span style="font-weight: bold;">', '</span>'),
|
'b': ('<span style="font-weight: bold;">', '</span>'),
|
||||||
'l': ('<span style="font-size: 150%;">', '</span>'),
|
'l': ('<span style="font-size: 150%;">', '</span>'),
|
||||||
'k': ('<span style="font-size: 75%;">', '</span>'),
|
'k': ('<span style="font-size: 75%; font-variant: small-caps;">', '</span>'),
|
||||||
'FN': ('<br /><br style="page-break-after: always;" /><div id="fn-%s"><p>', '</p><<small><a href="#rfn-%s">return</a></small></div>'),
|
'FN': ('<br /><br style="page-break-after: always;" /><div id="fn-%s"><p>', '</p><<small><a href="#rfn-%s">return</a></small></div>'),
|
||||||
'SB': ('<br /><br style="page-break-after: always;" /><div id="sb-%s"><p>', '</p><small><a href="#rsb-%s">return</a></small></div>'),
|
'SB': ('<br /><br style="page-break-after: always;" /><div id="sb-%s"><p>', '</p><small><a href="#rsb-%s">return</a></small></div>'),
|
||||||
}
|
}
|
||||||
@ -154,6 +154,11 @@ class PML_HTMLizer(object):
|
|||||||
self.file_name = ''
|
self.file_name = ''
|
||||||
|
|
||||||
def prepare_pml(self, pml):
|
def prepare_pml(self, pml):
|
||||||
|
# Give Chapters the form \\*='text'text\\*. This is used for generating
|
||||||
|
# the TOC later.
|
||||||
|
pml = re.sub(r'(?<=\\x)(?P<text>.*?)(?=\\x)', lambda match: '="%s"%s' % (self.strip_pml(match.group('text')), match.group('text')), pml)
|
||||||
|
pml = re.sub(r'(?<=\\X[0-4])(?P<text>.*?)(?=\\X[0-4])', lambda match: '="%s"%s' % (self.strip_pml(match.group('text')), match.group('text')), pml)
|
||||||
|
|
||||||
# Remove comments
|
# Remove comments
|
||||||
pml = re.sub(r'(?mus)\\v(?P<text>.*?)\\v', '', pml)
|
pml = re.sub(r'(?mus)\\v(?P<text>.*?)\\v', '', pml)
|
||||||
|
|
||||||
@ -163,7 +168,7 @@ class PML_HTMLizer(object):
|
|||||||
pml = re.sub(r'(?mus)(?<=.)[ ]*$', '', pml)
|
pml = re.sub(r'(?mus)(?<=.)[ ]*$', '', pml)
|
||||||
pml = re.sub(r'(?mus)^[ ]*$', '', pml)
|
pml = re.sub(r'(?mus)^[ ]*$', '', pml)
|
||||||
|
|
||||||
# Footnotes and Sidebars
|
# Footnotes and Sidebars.
|
||||||
pml = re.sub(r'(?mus)<footnote\s+id="(?P<target>.+?)">\s*(?P<text>.*?)\s*</footnote>', lambda match: '\\FN="%s"%s\\FN' % (match.group('target'), match.group('text')) if match.group('text') else '', pml)
|
pml = re.sub(r'(?mus)<footnote\s+id="(?P<target>.+?)">\s*(?P<text>.*?)\s*</footnote>', lambda match: '\\FN="%s"%s\\FN' % (match.group('target'), match.group('text')) if match.group('text') else '', pml)
|
||||||
pml = re.sub(r'(?mus)<sidebar\s+id="(?P<target>.+?)">\s*(?P<text>.*?)\s*</sidebar>', lambda match: '\\SB="%s"%s\\SB' % (match.group('target'), match.group('text')) if match.group('text') else '', pml)
|
pml = re.sub(r'(?mus)<sidebar\s+id="(?P<target>.+?)">\s*(?P<text>.*?)\s*</sidebar>', lambda match: '\\SB="%s"%s\\SB' % (match.group('target'), match.group('text')) if match.group('text') else '', pml)
|
||||||
|
|
||||||
@ -171,9 +176,7 @@ class PML_HTMLizer(object):
|
|||||||
# &. It will display as &
|
# &. It will display as &
|
||||||
pml = pml.replace('&', '&')
|
pml = pml.replace('&', '&')
|
||||||
|
|
||||||
pml = re.sub(r'(?<=\\x)(?P<text>.*?)(?=\\x)', lambda match: '="%s"%s' % (self.strip_pml(match.group('text')), match.group('text')), pml)
|
# Replace \\a and \\U with either the unicode character or the entity.
|
||||||
pml = re.sub(r'(?<=\\X[0-4])(?P<text>.*?)(?=\\X[0-4])', lambda match: '="%s"%s' % (self.strip_pml(match.group('text')), match.group('text')), pml)
|
|
||||||
|
|
||||||
pml = re.sub(r'\\a(?P<num>\d{3})', lambda match: '&#%s;' % match.group('num'), pml)
|
pml = re.sub(r'\\a(?P<num>\d{3})', lambda match: '&#%s;' % match.group('num'), pml)
|
||||||
pml = re.sub(r'\\U(?P<num>[0-9a-f]{4})', lambda match: '%s' % my_unichr(int(match.group('num'), 16)), pml)
|
pml = re.sub(r'\\U(?P<num>[0-9a-f]{4})', lambda match: '%s' % my_unichr(int(match.group('num'), 16)), pml)
|
||||||
|
|
||||||
@ -536,6 +539,7 @@ class PML_HTMLizer(object):
|
|||||||
elif '%s%s' % (c, l) == 'Sd':
|
elif '%s%s' % (c, l) == 'Sd':
|
||||||
text = self.process_code('Sd', line, 'sb')
|
text = self.process_code('Sd', line, 'sb')
|
||||||
elif c in 'xXC':
|
elif c in 'xXC':
|
||||||
|
empty = False
|
||||||
# The PML was modified eariler so x and X put the text
|
# The PML was modified eariler so x and X put the text
|
||||||
# inside of ="" so we don't have do special processing
|
# inside of ="" so we don't have do special processing
|
||||||
# for C.
|
# for C.
|
||||||
@ -578,9 +582,6 @@ class PML_HTMLizer(object):
|
|||||||
else:
|
else:
|
||||||
if c != ' ':
|
if c != ' ':
|
||||||
empty = False
|
empty = False
|
||||||
if self.state['k'][0]:
|
|
||||||
text = c.upper()
|
|
||||||
else:
|
|
||||||
text = c
|
text = c
|
||||||
parsed.append(text)
|
parsed.append(text)
|
||||||
c = line.read(1)
|
c = line.read(1)
|
||||||
|
@ -8,7 +8,7 @@ from functools import partial
|
|||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
|
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
|
||||||
Qt
|
Qt, pyqtSignal
|
||||||
|
|
||||||
from calibre.customize.ui import available_input_formats, available_output_formats, \
|
from calibre.customize.ui import available_input_formats, available_output_formats, \
|
||||||
device_plugins
|
device_plugins
|
||||||
@ -218,6 +218,16 @@ class DeviceManager(Thread):
|
|||||||
'''Return callable that returns the list of books on device as two booklists'''
|
'''Return callable that returns the list of books on device as two booklists'''
|
||||||
return self.create_job(self._books, done, description=_('Get list of books on device'))
|
return self.create_job(self._books, done, description=_('Get list of books on device'))
|
||||||
|
|
||||||
|
def _annotations(self, path_map):
|
||||||
|
return self.device.get_annotations(path_map)
|
||||||
|
|
||||||
|
def annotations(self, done, path_map):
|
||||||
|
'''Return mapping of ids to annotations. Each annotation is of the
|
||||||
|
form (type, location_info, content). path_map is a mapping of
|
||||||
|
ids to paths on the device.'''
|
||||||
|
return self.create_job(self._annotations, done, args=[path_map],
|
||||||
|
description=_('Get annotations from device'))
|
||||||
|
|
||||||
def _sync_booklists(self, booklists):
|
def _sync_booklists(self, booklists):
|
||||||
'''Sync metadata to device'''
|
'''Sync metadata to device'''
|
||||||
self.device.sync_booklists(booklists, end_session=False)
|
self.device.sync_booklists(booklists, end_session=False)
|
||||||
@ -298,6 +308,8 @@ class DeviceAction(QAction):
|
|||||||
|
|
||||||
class DeviceMenu(QMenu):
|
class DeviceMenu(QMenu):
|
||||||
|
|
||||||
|
fetch_annotations = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QMenu.__init__(self, parent)
|
QMenu.__init__(self, parent)
|
||||||
self.group = QActionGroup(self)
|
self.group = QActionGroup(self)
|
||||||
@ -389,10 +401,16 @@ class DeviceMenu(QMenu):
|
|||||||
|
|
||||||
self.connect(self.group, SIGNAL('triggered(QAction*)'),
|
self.connect(self.group, SIGNAL('triggered(QAction*)'),
|
||||||
self.change_default_action)
|
self.change_default_action)
|
||||||
self.enable_device_actions(False)
|
|
||||||
if opts.accounts:
|
if opts.accounts:
|
||||||
self.addSeparator()
|
self.addSeparator()
|
||||||
self.addMenu(self.email_to_menu)
|
self.addMenu(self.email_to_menu)
|
||||||
|
self.addSeparator()
|
||||||
|
annot = self.addAction(_('Fetch annotations (experimental)'))
|
||||||
|
annot.setEnabled(False)
|
||||||
|
annot.triggered.connect(lambda x :
|
||||||
|
self.fetch_annotations.emit())
|
||||||
|
self.annotation_action = annot
|
||||||
|
self.enable_device_actions(False)
|
||||||
|
|
||||||
def change_default_action(self, action):
|
def change_default_action(self, action):
|
||||||
config['default_send_to_device_action'] = repr(action)
|
config['default_send_to_device_action'] = repr(action)
|
||||||
@ -409,7 +427,8 @@ class DeviceMenu(QMenu):
|
|||||||
self.action_triggered(action)
|
self.action_triggered(action)
|
||||||
break
|
break
|
||||||
|
|
||||||
def enable_device_actions(self, enable, card_prefix=(None, None)):
|
def enable_device_actions(self, enable, card_prefix=(None, None),
|
||||||
|
device=None):
|
||||||
for action in self.actions:
|
for action in self.actions:
|
||||||
if action.dest in ('main:', 'carda:0', 'cardb:0'):
|
if action.dest in ('main:', 'carda:0', 'cardb:0'):
|
||||||
if not enable:
|
if not enable:
|
||||||
@ -428,6 +447,9 @@ class DeviceMenu(QMenu):
|
|||||||
else:
|
else:
|
||||||
action.setEnabled(False)
|
action.setEnabled(False)
|
||||||
|
|
||||||
|
annot_enable = enable and getattr(device, 'SUPPORTS_ANNOTATIONS', False)
|
||||||
|
self.annotation_action.setEnabled(annot_enable)
|
||||||
|
|
||||||
|
|
||||||
class Emailer(Thread):
|
class Emailer(Thread):
|
||||||
|
|
||||||
|
@ -40,7 +40,6 @@ class ProgressDialog(QDialog, Ui_Dialog):
|
|||||||
return property(fget=fget, fset=fset)
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def set_min(self, min):
|
def set_min(self, min):
|
||||||
self.bar.setMinimum(min)
|
self.bar.setMinimum(min)
|
||||||
|
|
||||||
|
@ -9,7 +9,8 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
'''The main GUI'''
|
'''The main GUI'''
|
||||||
|
|
||||||
import os, shutil, sys, textwrap, collections, time
|
import collections, datetime, os, shutil, sys, textwrap, time
|
||||||
|
from collections import namedtuple
|
||||||
from xml.parsers.expat import ExpatError
|
from xml.parsers.expat import ExpatError
|
||||||
from Queue import Queue, Empty
|
from Queue import Queue, Empty
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@ -18,10 +19,11 @@ from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \
|
|||||||
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
|
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
|
||||||
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
||||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||||
QMessageBox, QStackedLayout, QHelpEvent, QInputDialog
|
QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\
|
||||||
|
QThread
|
||||||
from PyQt4.QtSvg import QSvgRenderer
|
from PyQt4.QtSvg import QSvgRenderer
|
||||||
|
|
||||||
from calibre import prints, patheq
|
from calibre import prints, patheq, strftime
|
||||||
from calibre.constants import __version__, __appname__, isfrozen, islinux, \
|
from calibre.constants import __version__, __appname__, isfrozen, islinux, \
|
||||||
iswindows, isosx, filesystem_encoding
|
iswindows, isosx, filesystem_encoding
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
@ -54,6 +56,8 @@ from calibre.gui2.dialogs.search import SearchDialog
|
|||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.gui2.dialogs.book_info import BookInfo
|
from calibre.gui2.dialogs.book_info import BookInfo
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
||||||
|
from calibre.library.cli import send_message as calibre_send_message
|
||||||
from calibre.library.database2 import LibraryDatabase2, CoverCache
|
from calibre.library.database2 import LibraryDatabase2, CoverCache
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
|
||||||
@ -617,6 +621,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.dispatch_sync_event)
|
self.dispatch_sync_event)
|
||||||
self.connect(self.action_sync, SIGNAL('triggered(bool)'),
|
self.connect(self.action_sync, SIGNAL('triggered(bool)'),
|
||||||
self._sync_menu.trigger_default)
|
self._sync_menu.trigger_default)
|
||||||
|
self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
|
||||||
|
|
||||||
def add_spare_server(self, *args):
|
def add_spare_server(self, *args):
|
||||||
self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0)))
|
self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0)))
|
||||||
@ -855,7 +860,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.device_manager.device.__class__.get_gui_name()+\
|
self.device_manager.device.__class__.get_gui_name()+\
|
||||||
_(' detected.'), 3000)
|
_(' detected.'), 3000)
|
||||||
self.device_connected = True
|
self.device_connected = True
|
||||||
self._sync_menu.enable_device_actions(True, self.device_manager.device.card_prefix())
|
self._sync_menu.enable_device_actions(True,
|
||||||
|
self.device_manager.device.card_prefix(),
|
||||||
|
self.device_manager.device)
|
||||||
self.location_view.model().device_connected(self.device_manager.device)
|
self.location_view.model().device_connected(self.device_manager.device)
|
||||||
else:
|
else:
|
||||||
self.save_device_view_settings()
|
self.save_device_view_settings()
|
||||||
@ -918,7 +925,163 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.sync_catalogs()
|
self.sync_catalogs()
|
||||||
############################################################################
|
############################################################################
|
||||||
|
|
||||||
|
######################### Fetch annotations ################################
|
||||||
|
|
||||||
|
def fetch_annotations(self, *args):
|
||||||
|
# Figure out a list of ids using the same logic as the catalog generation
|
||||||
|
# FUnction. Use the currently connected device to map ids to paths
|
||||||
|
|
||||||
|
def get_ids_from_selected_rows():
|
||||||
|
rows = self.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) < 2:
|
||||||
|
rows = xrange(self.library_view.model().rowCount(QModelIndex()))
|
||||||
|
ids = map(self.library_view.model().id, rows)
|
||||||
|
return ids
|
||||||
|
|
||||||
|
def generate_annotation_paths(ids, db, device):
|
||||||
|
# Generate a dict {1:'documents/documents/Asimov, Isaac/Foundation - Isaac Asimov.epub'}
|
||||||
|
# These are the not the absolute paths - individual storage mount points will need to be
|
||||||
|
# prepended during the search
|
||||||
|
path_map = {}
|
||||||
|
for id in ids:
|
||||||
|
mi = db.get_metadata(id, index_is_id=True)
|
||||||
|
a_path = device.create_upload_path(os.path.abspath('/<storage>'), mi, 'x.mbp', create_dirs=False)
|
||||||
|
path_map[id] = a_path
|
||||||
|
return path_map
|
||||||
|
|
||||||
|
device = self.device_manager.device
|
||||||
|
|
||||||
|
if self.current_view() is not self.library_view:
|
||||||
|
return error_dialog(self, _('Use library only'),
|
||||||
|
_('User annotations generated from main library only'),
|
||||||
|
show=True)
|
||||||
|
db = self.library_view.model().db
|
||||||
|
|
||||||
|
# Get the list of ids
|
||||||
|
ids = get_ids_from_selected_rows()
|
||||||
|
if not ids:
|
||||||
|
return error_dialog(self, _('No books selected'),
|
||||||
|
_('No books selected to fetch annotations from'),
|
||||||
|
show=True)
|
||||||
|
|
||||||
|
# Map ids to paths
|
||||||
|
path_map = generate_annotation_paths(ids, db, device)
|
||||||
|
|
||||||
|
# Dispatch to devices.kindle.driver.get_annotations()
|
||||||
|
self.device_manager.annotations(Dispatcher(self.annotations_fetched),
|
||||||
|
path_map)
|
||||||
|
|
||||||
|
def annotations_fetched(self, job):
|
||||||
|
from calibre.devices.usbms.device import Device
|
||||||
|
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||||
|
|
||||||
|
class Updater(QThread):
|
||||||
|
def __init__(self, parent, db, annotation_map):
|
||||||
|
QThread.__init__(self, parent)
|
||||||
|
self.db = db
|
||||||
|
self.pd = ProgressDialog(_('Merging user annotations into database'), '',
|
||||||
|
0, len(job.result), parent=parent)
|
||||||
|
|
||||||
|
self.am = annotation_map
|
||||||
|
self.connect(self.pd, SIGNAL('canceled()'), self.canceled)
|
||||||
|
self.pd.setModal(True)
|
||||||
|
self.pd.show()
|
||||||
|
|
||||||
|
def generate_annotation_html(self, bookmark):
|
||||||
|
# Returns <div class="user_annotations"> ... </div>
|
||||||
|
last_read_location = bookmark.last_read_location
|
||||||
|
timestamp = datetime.datetime.utcfromtimestamp(bookmark.timestamp)
|
||||||
|
percent_read = bookmark.percent_read
|
||||||
|
|
||||||
|
ka_soup = BeautifulSoup()
|
||||||
|
dtc = 0
|
||||||
|
divTag = Tag(ka_soup,'div')
|
||||||
|
divTag['class'] = 'user_annotations'
|
||||||
|
|
||||||
|
# Add the last-read location
|
||||||
|
spanTag = Tag(ka_soup, 'span')
|
||||||
|
spanTag['style'] = 'font-weight:bold'
|
||||||
|
spanTag.insert(0,NavigableString("%s<br />Last Page Read: Location %d (%d%%)" % \
|
||||||
|
(strftime(u'%x', timestamp.timetuple()),
|
||||||
|
last_read_location/150 + 1,
|
||||||
|
percent_read)))
|
||||||
|
|
||||||
|
divTag.insert(dtc, spanTag)
|
||||||
|
dtc += 1
|
||||||
|
divTag.insert(dtc, Tag(ka_soup,'br'))
|
||||||
|
dtc += 1
|
||||||
|
|
||||||
|
if bookmark.user_notes:
|
||||||
|
user_notes = bookmark.user_notes
|
||||||
|
annotations = []
|
||||||
|
|
||||||
|
# Add the annotations sorted by location
|
||||||
|
# Italicize highlighted text
|
||||||
|
for location in sorted(user_notes):
|
||||||
|
if user_notes[location]['text']:
|
||||||
|
annotations.append('<b>Location %d • %s</b><br />%s<br />' % \
|
||||||
|
(location/150 + 1,
|
||||||
|
user_notes[location]['type'],
|
||||||
|
user_notes[location]['text'] if \
|
||||||
|
user_notes[location]['type'] == 'Note' else \
|
||||||
|
'<i>%s</i>' % user_notes[location]['text']))
|
||||||
|
else:
|
||||||
|
annotations.append('<b>Location %d • %s</b><br />' % \
|
||||||
|
(location/150 + 1,
|
||||||
|
user_notes[location]['type']))
|
||||||
|
|
||||||
|
for annotation in annotations:
|
||||||
|
divTag.insert(dtc, annotation)
|
||||||
|
dtc += 1
|
||||||
|
|
||||||
|
ka_soup.insert(0,divTag)
|
||||||
|
return ka_soup
|
||||||
|
|
||||||
|
def canceled(self):
|
||||||
|
self.pd.hide()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
QApplication.processEvents()
|
||||||
|
for (i, id) in enumerate(self.am):
|
||||||
|
bm = Device.UserAnnotation(self.am[id][0],self.am[id][1])
|
||||||
|
user_notes_soup = self.generate_annotation_html(bm.bookmark)
|
||||||
|
|
||||||
|
mi = self.db.get_metadata(id, index_is_id=True)
|
||||||
|
a_offset = mi.comments.find('<div class="user_annotations">')
|
||||||
|
ad_offset = mi.comments.find('<hr class="annotations_divider" />')
|
||||||
|
|
||||||
|
if a_offset >= 0:
|
||||||
|
mi.comments = mi.comments[:a_offset]
|
||||||
|
if ad_offset >= 0:
|
||||||
|
mi.comments = mi.comments[:ad_offset]
|
||||||
|
if mi.comments:
|
||||||
|
hrTag = Tag(user_notes_soup,'hr')
|
||||||
|
hrTag['class'] = 'annotations_divider'
|
||||||
|
user_notes_soup.insert(0,hrTag)
|
||||||
|
|
||||||
|
mi.comments += user_notes_soup.prettify()
|
||||||
|
|
||||||
|
# Update library comments
|
||||||
|
self.db.set_comment(id, mi.comments)
|
||||||
|
|
||||||
|
self.pd.set_value(i)
|
||||||
|
self.pd.hide()
|
||||||
|
calibre_send_message()
|
||||||
|
|
||||||
|
if not job.result: return
|
||||||
|
|
||||||
|
if self.current_view() is not self.library_view:
|
||||||
|
return error_dialog(self, _('Use library only'),
|
||||||
|
_('User annotations generated from main library only'),
|
||||||
|
show=True)
|
||||||
|
db = self.library_view.model().db
|
||||||
|
|
||||||
|
self.__annotation_updater = Updater(self, db, job.result)
|
||||||
|
self.__annotation_updater.start()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
|
||||||
################################# Add books ################################
|
################################# Add books ################################
|
||||||
|
|
||||||
|
@ -925,11 +925,6 @@ class EPUB_MOBI(CatalogPlugin):
|
|||||||
return False
|
return False
|
||||||
self.fetchBooksByAuthor()
|
self.fetchBooksByAuthor()
|
||||||
self.fetchBookmarks()
|
self.fetchBookmarks()
|
||||||
|
|
||||||
updateLibraryComments = True
|
|
||||||
if updateLibraryComments:
|
|
||||||
self.updateLibraryComments()
|
|
||||||
|
|
||||||
self.generateHTMLDescriptions()
|
self.generateHTMLDescriptions()
|
||||||
self.generateHTMLByAuthor()
|
self.generateHTMLByAuthor()
|
||||||
if self.opts.generate_titles:
|
if self.opts.generate_titles:
|
||||||
@ -1180,10 +1175,12 @@ class EPUB_MOBI(CatalogPlugin):
|
|||||||
Preferences|Add/Save|Sending to device, not a customized one specified in
|
Preferences|Add/Save|Sending to device, not a customized one specified in
|
||||||
the Kindle plugin
|
the Kindle plugin
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
|
||||||
from calibre.devices.usbms.device import Device
|
from calibre.devices.usbms.device import Device
|
||||||
|
from calibre.devices.kindle.driver import Bookmark
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.ebooks.metadata.mobi import StreamSlicer
|
from calibre.ebooks.metadata.mobi import StreamSlicer
|
||||||
|
|
||||||
@ -1194,170 +1191,12 @@ class EPUB_MOBI(CatalogPlugin):
|
|||||||
def save_template(self):
|
def save_template(self):
|
||||||
return self._save_template
|
return self._save_template
|
||||||
|
|
||||||
class Bookmark():
|
|
||||||
'''
|
|
||||||
A simple class storing bookmark data
|
|
||||||
Kindle-specific
|
|
||||||
'''
|
|
||||||
def __init__(self, path, formats, id):
|
|
||||||
self.book_format = None
|
|
||||||
self.book_length = 0
|
|
||||||
self.id = id
|
|
||||||
self.last_read_location = 0
|
|
||||||
self.timestamp = 0
|
|
||||||
self.user_notes = None
|
|
||||||
|
|
||||||
self.get_bookmark_data(path)
|
|
||||||
self.get_book_length(path, formats)
|
|
||||||
|
|
||||||
def record(self, n):
|
|
||||||
if n >= self.nrecs:
|
|
||||||
raise ValueError('non-existent record %r' % n)
|
|
||||||
offoff = 78 + (8 * n)
|
|
||||||
start, = unpack('>I', self.data[offoff + 0:offoff + 4])
|
|
||||||
stop = None
|
|
||||||
if n < (self.nrecs - 1):
|
|
||||||
stop, = unpack('>I', self.data[offoff + 8:offoff + 12])
|
|
||||||
return StreamSlicer(self.stream, start, stop)
|
|
||||||
|
|
||||||
def hexdump(self, src, length=16):
|
|
||||||
# Diagnostic
|
|
||||||
FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
|
|
||||||
N=0; result=''
|
|
||||||
while src:
|
|
||||||
s,src = src[:length],src[length:]
|
|
||||||
hexa = ' '.join(["%02X"%ord(x) for x in s])
|
|
||||||
s = s.translate(FILTER)
|
|
||||||
result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
|
|
||||||
N+=length
|
|
||||||
print result
|
|
||||||
|
|
||||||
def textdump(self, src, width=80, indent=5):
|
|
||||||
tokens = src.split(' ')
|
|
||||||
result='%s' % (' ' * indent)
|
|
||||||
results = []
|
|
||||||
while tokens:
|
|
||||||
result += tokens[0].decode('mac-roman') + ' '
|
|
||||||
tokens.pop(0)
|
|
||||||
if len(result) > width:
|
|
||||||
results.append(result)
|
|
||||||
result='%s' % (' ' * indent)
|
|
||||||
if result.strip():
|
|
||||||
results.append(result)
|
|
||||||
return results
|
|
||||||
|
|
||||||
def get_bookmark_data(self, path, fetchUserNotes=True):
|
|
||||||
''' Return the timestamp and last_read_location '''
|
|
||||||
with open(path,'rb') as f:
|
|
||||||
stream = StringIO(f.read())
|
|
||||||
data = StreamSlicer(stream)
|
|
||||||
self.timestamp, = unpack('>I', data[0x24:0x28])
|
|
||||||
bpar_offset, = unpack('>I', data[0x4e:0x52])
|
|
||||||
lrlo = bpar_offset + 0x0c
|
|
||||||
self.last_read_location = int(unpack('>I', data[lrlo:lrlo+4])[0])
|
|
||||||
entries, = unpack('>I', data[0x4a:0x4e])
|
|
||||||
|
|
||||||
# Store the annotations/locations
|
|
||||||
if fetchUserNotes:
|
|
||||||
bpl = bpar_offset + 4
|
|
||||||
bpar_len, = unpack('>I', data[bpl:bpl+4])
|
|
||||||
bpar_len += 8
|
|
||||||
#print "bpar_len: 0x%x" % bpar_len
|
|
||||||
eo = bpar_offset + bpar_len
|
|
||||||
|
|
||||||
# Walk bookmark entries
|
|
||||||
#print " --- %s --- " % path
|
|
||||||
#print " last_read_location: %d" % self.magicKindleLocationCalculator(last_read_location)
|
|
||||||
current_entry = 1
|
|
||||||
sig = data[eo:eo+4]
|
|
||||||
previous_block = None
|
|
||||||
user_notes = {}
|
|
||||||
|
|
||||||
while sig == 'DATA':
|
|
||||||
text = None
|
|
||||||
entry_type = None
|
|
||||||
rec_len, = unpack('>I', data[eo+4:eo+8])
|
|
||||||
if rec_len == 0:
|
|
||||||
current_block = "empty_data"
|
|
||||||
elif data[eo+8:eo+12] == "EBAR":
|
|
||||||
current_block = "data_header"
|
|
||||||
#entry_type = "data_header"
|
|
||||||
location, = unpack('>I', data[eo+0x34:eo+0x38])
|
|
||||||
#print "data_header location: %d" % location
|
|
||||||
else:
|
|
||||||
current_block = "text_block"
|
|
||||||
if previous_block == 'empty_data':
|
|
||||||
entry_type = 'Note'
|
|
||||||
elif previous_block == 'data_header':
|
|
||||||
entry_type = 'Highlight'
|
|
||||||
text = data[eo+8:eo+8+rec_len].decode('utf-16-be')
|
|
||||||
|
|
||||||
if entry_type:
|
|
||||||
user_notes[location] = dict(type=entry_type, id=self.id,
|
|
||||||
text=data[eo+8:eo+8+rec_len].decode('utf-16-be'))
|
|
||||||
#print " %2d: %s %s" % (current_entry, entry_type,'at %d' % location if location else '')
|
|
||||||
#if current_block == 'text_block':
|
|
||||||
#self.textdump(text)
|
|
||||||
|
|
||||||
eo += rec_len + 8
|
|
||||||
current_entry += 1
|
|
||||||
previous_block = current_block
|
|
||||||
sig = data[eo:eo+4]
|
|
||||||
|
|
||||||
while sig == 'BKMK':
|
|
||||||
# Fix start location for Highlights using BKMK data
|
|
||||||
end_loc, = unpack('>I', data[eo+0x10:eo+0x14])
|
|
||||||
if end_loc in user_notes and user_notes[end_loc]['type'] != 'Note':
|
|
||||||
start, = unpack('>I', data[eo+8:eo+12])
|
|
||||||
user_notes[start] = user_notes[end_loc]
|
|
||||||
user_notes.pop(end_loc)
|
|
||||||
#print "changing start location of %d to %d" % (end_loc,start)
|
|
||||||
else:
|
|
||||||
# If a bookmark coincides with a user annotation, the locs could
|
|
||||||
# be the same - cheat by nudging -1
|
|
||||||
# Skip bookmark for last_read_location
|
|
||||||
if end_loc != self.last_read_location:
|
|
||||||
user_notes[end_loc - 1] = dict(type='Bookmark',id=self.id,text=None)
|
|
||||||
rec_len, = unpack('>I', data[eo+4:eo+8])
|
|
||||||
eo += rec_len + 8
|
|
||||||
sig = data[eo:eo+4]
|
|
||||||
|
|
||||||
'''
|
|
||||||
for location in sorted(user_notes):
|
|
||||||
print ' Location %d: %s\n%s' % self.magicKindleLocationCalculator(location),
|
|
||||||
user_notes[location]['type'],
|
|
||||||
'\n'.join(self.textdump(user_notes[location]['text'])))
|
|
||||||
'''
|
|
||||||
self.user_notes = user_notes
|
|
||||||
|
|
||||||
def get_book_length(self, path, formats):
|
|
||||||
# This assumes only one of the possible formats exists on the Kindle
|
|
||||||
book_fs = None
|
|
||||||
for format in formats:
|
|
||||||
fmt = format.rpartition('.')[2]
|
|
||||||
if fmt in ['mobi','prc','azw']:
|
|
||||||
book_fs = path.replace('.mbp','.%s' % fmt)
|
|
||||||
if os.path.exists(book_fs):
|
|
||||||
self.book_format = fmt
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
#print "no files matching library formats exist on device"
|
|
||||||
self.book_length = 0
|
|
||||||
return
|
|
||||||
|
|
||||||
# Read the book len from the header
|
|
||||||
with open(book_fs,'rb') as f:
|
|
||||||
self.stream = StringIO(f.read())
|
|
||||||
self.data = StreamSlicer(self.stream)
|
|
||||||
self.nrecs, = unpack('>H', self.data[76:78])
|
|
||||||
record0 = self.record(0)
|
|
||||||
self.book_length = int(unpack('>I', record0[0x04:0x08])[0])
|
|
||||||
|
|
||||||
if self.generateRecentlyRead:
|
if self.generateRecentlyRead:
|
||||||
self.opts.log.info(" Collecting Kindle bookmarks matching catalog entries")
|
self.opts.log.info(" Collecting Kindle bookmarks matching catalog entries")
|
||||||
|
|
||||||
d = BookmarkDevice(None)
|
d = BookmarkDevice(None)
|
||||||
d.initialize(self.opts.connected_device['save_template'])
|
d.initialize(self.opts.connected_device['save_template'])
|
||||||
|
|
||||||
bookmarks = {}
|
bookmarks = {}
|
||||||
for book in self.booksByTitle:
|
for book in self.booksByTitle:
|
||||||
original_title = book['title'][book['title'].find(':') + 2:] if book['series'] \
|
original_title = book['title'][book['title'].find(':') + 2:] if book['series'] \
|
||||||
@ -1380,7 +1219,6 @@ class EPUB_MOBI(CatalogPlugin):
|
|||||||
bm_found = True
|
bm_found = True
|
||||||
if bm_found:
|
if bm_found:
|
||||||
break
|
break
|
||||||
|
|
||||||
self.bookmarked_books = bookmarks
|
self.bookmarked_books = bookmarks
|
||||||
else:
|
else:
|
||||||
self.bookmarked_books = {}
|
self.bookmarked_books = {}
|
||||||
@ -3365,98 +3203,6 @@ class EPUB_MOBI(CatalogPlugin):
|
|||||||
|
|
||||||
self.ncxSoup = ncx_soup
|
self.ncxSoup = ncx_soup
|
||||||
|
|
||||||
def updateLibraryComments(self):
|
|
||||||
# Append user notes to library book['comments'], catalog book['description']
|
|
||||||
from calibre.library.cli import send_message as calibre_send_message
|
|
||||||
|
|
||||||
if self.bookmarked_books:
|
|
||||||
annotations_header = '<br /><hr class="series_divider" />' + \
|
|
||||||
'<i>Kindle Annotations</i><br />'
|
|
||||||
for id in self.bookmarked_books:
|
|
||||||
last_read_location = self.bookmarked_books[id][0].last_read_location
|
|
||||||
timestamp = datetime.datetime.utcfromtimestamp(self.bookmarked_books[id][0].timestamp)
|
|
||||||
reading_progress = self.bookmarked_books[id][1]['reading_progress']
|
|
||||||
percent_read = self.bookmarked_books[id][1]['percent_read']
|
|
||||||
ka_soup = BeautifulSoup()
|
|
||||||
dtc = 0
|
|
||||||
divTag = Tag(ka_soup,'div')
|
|
||||||
divTag['class'] = 'kindle_annotations'
|
|
||||||
|
|
||||||
# Add the last-read location
|
|
||||||
spanTag = Tag(ka_soup, 'span')
|
|
||||||
spanTag['style'] = 'font-weight:bold'
|
|
||||||
spanTag.insert(0,NavigableString("%s %s<br />Last Page Read: Location %d (%d%%)" % \
|
|
||||||
(strftime(u'%x', timestamp.timetuple()),
|
|
||||||
reading_progress,
|
|
||||||
self.magicKindleLocationCalculator(last_read_location),
|
|
||||||
percent_read)))
|
|
||||||
|
|
||||||
divTag.insert(dtc, spanTag)
|
|
||||||
dtc += 1
|
|
||||||
divTag.insert(dtc, Tag(ka_soup,'br'))
|
|
||||||
dtc += 1
|
|
||||||
|
|
||||||
if self.bookmarked_books[id][0].user_notes:
|
|
||||||
user_notes = self.bookmarked_books[id][0].user_notes
|
|
||||||
annotations = []
|
|
||||||
|
|
||||||
if False:
|
|
||||||
spanTag = Tag(ka_soup, 'span')
|
|
||||||
spanTag['style'] = 'font-style:italic;font-weight:bold;text-align:right'
|
|
||||||
spanTag.insert(0,NavigableString("Kindle Annotations"))
|
|
||||||
divTag.insert(dtc, spanTag)
|
|
||||||
dtc += 1
|
|
||||||
divTag.insert(dtc, Tag(ka_soup,'br'))
|
|
||||||
dtc += 1
|
|
||||||
|
|
||||||
# Add the annotations sorted by location
|
|
||||||
# Italicize highlighted text
|
|
||||||
for location in sorted(user_notes):
|
|
||||||
if user_notes[location]['text']:
|
|
||||||
annotations.append('<b>Location %d • %s</b><br />%s<br />' % \
|
|
||||||
(self.magicKindleLocationCalculator(location),
|
|
||||||
user_notes[location]['type'],
|
|
||||||
user_notes[location]['text'] if \
|
|
||||||
user_notes[location]['type'] == 'Note' else \
|
|
||||||
'<i>%s</i>' % user_notes[location]['text']))
|
|
||||||
else:
|
|
||||||
annotations.append('<b>Location %d • %s</b><br />' % \
|
|
||||||
(self.magicKindleLocationCalculator(location),
|
|
||||||
user_notes[location]['type']))
|
|
||||||
|
|
||||||
for annotation in annotations:
|
|
||||||
divTag.insert(dtc, annotation)
|
|
||||||
dtc += 1
|
|
||||||
|
|
||||||
ka_soup.insert(0,divTag)
|
|
||||||
|
|
||||||
mi = self.db.get_metadata(id, index_is_id=True)
|
|
||||||
ka_offset = mi.comments.find('<div class="kindle_annotations">')
|
|
||||||
kad_offset = mi.comments.find('<hr class="annotations_divider" />')
|
|
||||||
|
|
||||||
if ka_offset >= 0:
|
|
||||||
mi.comments = mi.comments[:ka_offset]
|
|
||||||
if kad_offset >= 0:
|
|
||||||
mi.comments = mi.comments[:kad_offset]
|
|
||||||
if mi.comments:
|
|
||||||
hrTag = Tag(ka_soup,'hr')
|
|
||||||
hrTag['class'] = 'annotations_divider'
|
|
||||||
ka_soup.insert(0,hrTag)
|
|
||||||
|
|
||||||
mi.comments += ka_soup.prettify()
|
|
||||||
|
|
||||||
# Update library comments
|
|
||||||
self.db.set_comment(id, mi.comments)
|
|
||||||
calibre_send_message()
|
|
||||||
|
|
||||||
# Update catalog description prior to build
|
|
||||||
# This might be better to do during fetchBooksByTitle?
|
|
||||||
# Try self.bookmarked_books[id][1]['description']
|
|
||||||
for title in self.booksByTitle:
|
|
||||||
if title['id'] == id:
|
|
||||||
title['description'] = mi.comments
|
|
||||||
break
|
|
||||||
|
|
||||||
def writeNCX(self):
|
def writeNCX(self):
|
||||||
self.updateProgressFullStep("Saving NCX")
|
self.updateProgressFullStep("Saving NCX")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user