From b857fd3fd13a3cf57d9f6cd3231898444f00b382 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 9 Apr 2011 20:01:49 -0400 Subject: [PATCH 01/77] Start of plucker input support. --- src/calibre/ebooks/pdb/__init__.py | 4 +- src/calibre/ebooks/pdb/plucker/__init__.py | 0 src/calibre/ebooks/pdb/plucker/reader.py | 149 +++++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/calibre/ebooks/pdb/plucker/__init__.py create mode 100644 src/calibre/ebooks/pdb/plucker/reader.py diff --git a/src/calibre/ebooks/pdb/__init__.py b/src/calibre/ebooks/pdb/__init__.py index 092c8a21bd..c8089297db 100644 --- a/src/calibre/ebooks/pdb/__init__.py +++ b/src/calibre/ebooks/pdb/__init__.py @@ -12,6 +12,7 @@ from calibre.ebooks.pdb.ereader.reader import Reader as ereader_reader from calibre.ebooks.pdb.palmdoc.reader import Reader as palmdoc_reader from calibre.ebooks.pdb.ztxt.reader import Reader as ztxt_reader from calibre.ebooks.pdb.pdf.reader import Reader as pdf_reader +from calibre.ebooks.pdb.plucker.reader import Reader as plucker_reader FORMAT_READERS = { 'PNPdPPrs': ereader_reader, @@ -19,6 +20,7 @@ FORMAT_READERS = { 'zTXTGPlm': ztxt_reader, 'TEXtREAd': palmdoc_reader, '.pdfADBE': pdf_reader, + 'DataPlkr': plucker_reader, } from calibre.ebooks.pdb.palmdoc.writer import Writer as palmdoc_writer @@ -37,6 +39,7 @@ IDENTITY_TO_NAME = { 'zTXTGPlm': 'zTXT', 'TEXtREAd': 'PalmDOC', '.pdfADBE': 'Adobe Reader', + 'DataPlkr': 'Plucker', 'BVokBDIC': 'BDicty', 'DB99DBOS': 'DB (Database program)', @@ -50,7 +53,6 @@ IDENTITY_TO_NAME = { 'DATALSdb': 'LIST', 'Mdb1Mdb1': 'MobileDB', 'BOOKMOBI': 'MobiPocket', - 'DataPlkr': 'Plucker', 'DataSprd': 'QuickSheet', 'SM01SMem': 'SuperMemo', 'TEXtTlDc': 'TealDoc', diff --git a/src/calibre/ebooks/pdb/plucker/__init__.py b/src/calibre/ebooks/pdb/plucker/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py new file mode 100644 index 0000000000..d1e5931580 --- /dev/null +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +#from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL v3' +__copyright__ = '20011, John Schember ' +__docformat__ = 'restructuredtext en' + +import os +import struct +import zlib + +from calibre import CurrentDir +from calibre.ebooks.metadata.opf2 import OPFCreator +from calibre.ebooks.pdb.formatreader import FormatReader + +DATATYPE_PHTML = 0 +DATATYPE_PHTML_COMPRESSED = 1 +DATATYPE_TBMP = 2 +DATATYPE_TBMP_COMPRESSED = 3 +DATATYPE_MAILTO = 4 +DATATYPE_LINK_INDEX = 5 +DATATYPE_LINKS = 6 +DATATYPE_LINKS_COMPRESSED = 7 +DATATYPE_BOOKMARKS = 8 +DATATYPE_CATEGORY = 9 +DATATYPE_METADATA = 10 +DATATYPE_STYLE_SHEET = 11 +DATATYPE_FONT_PAGE = 12 +DATATYPE_TABLE = 13 +DATATYPE_TABLE_COMPRESSED = 14 +DATATYPE_COMPOSITE_IMAGE = 15 +DATATYPE_PAGELIST_METADATA = 16 +DATATYPE_SORTED_URL_INDEX = 17 +DATATYPE_SORTED_URL = 18 +DATATYPE_SORTED_URL_COMPRESSED = 19 +DATATYPE_EXT_ANCHOR_INDEX = 20 +DATATYPE_EXT_ANCHOR = 21 +DATATYPE_EXT_ANCHOR_COMPRESSED = 22 + +class HeaderRecord(object): + + def __init__(self, raw): + self.uid, = struct.unpack('>H', raw[0:2]) + # This is labled version in the spec. + # 2 is ZLIB compressed, + # 1 is DOC compressed + self.compression, = struct.unpack('>H', raw[2:4]) + self.records, = struct.unpack('>H', raw[4:6]) + + self.reserved = {} + for i in xrange(self.records): + adv = 4*i + name, = struct.unpack('>H', raw[6+adv:8+adv]) + id, = struct.unpack('>H', raw[8+adv:10+adv]) + self.reserved[id] = name + + +class SectionHeader(object): + + def __init__(self, raw): + self.uid, = struct.unpack('>H', raw[0:2]) + self.paragraphs, = struct.unpack('>H', raw[2:4]) + self.size, = struct.unpack('>H', raw[4:6]) + self.type, = struct.unpack('>B', raw[6]) + self.flags, = struct.unpack('>B', raw[7]) + + +class SectionHeaderText(object): + + def __init__(self, data_header, raw): + self.sizes = [] + self.attributes = [] + + for i in xrange(data_header.paragraphs): + adv = 4*i + self.sizes.append(struct.unpack('>H', raw[8+adv:10+adv])[0]) + self.attributes.append(struct.unpack('>H', raw[10+adv:12+adv])[0]) + + +class Reader(FormatReader): + + def __init__(self, header, stream, log, options): + self.stream = stream + self.log = log + self.options = options + + self.sections = [] + for i in range(1, header.num_sections): + start = 8 + raw_data = header.section_data(i) + data_header = SectionHeader(raw_data) + sub_header = None + if data_header.type in (DATATYPE_PHTML, DATATYPE_PHTML_COMPRESSED): + sub_header = SectionHeaderText(data_header, raw_data) + start += data_header.paragraphs * 4 + self.sections.append((data_header, sub_header, raw_data[start:])) + + self.header_record = HeaderRecord(header.section_data(0)) + + from calibre.ebooks.metadata.pdb import get_metadata + self.mi = get_metadata(stream, False) + + def extract_content(self, output_dir): + html = u'' + images = [] + + for header, sub_header, data in self.sections: + if header.type == DATATYPE_PHTML: + html += data + elif header.type == DATATYPE_PHTML_COMPRESSED: + d = self.decompress_phtml(data).decode('latin-1', 'replace') + print len(d) == header.size + html += d + + print html + with CurrentDir(output_dir): + with open('index.html', 'wb') as index: + self.log.debug('Writing text to index.html') + index.write(html.encode('utf-8')) + + opf_path = self.create_opf(output_dir, images) + + return opf_path + + def decompress_phtml(self, data): + if self.header_record.compression == 2: + raise NotImplementedError + #return zlib.decompress(data) + elif self.header_record.compression == 1: + from calibre.ebooks.compression.palmdoc import decompress_doc + return decompress_doc(data) + + + def create_opf(self, output_dir, images): + with CurrentDir(output_dir): + opf = OPFCreator(output_dir, self.mi) + + manifest = [('index.html', None)] + + for i in images: + manifest.append((os.path.join('images/', i), None)) + + opf.create_manifest(manifest) + opf.create_spine(['index.html']) + with open('metadata.opf', 'wb') as opffile: + opf.render(opffile) + + return os.path.join(output_dir, 'metadata.opf') From 0f3228e6585dadcf6f4aa6110ed3619966bbfff2 Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 11 Apr 2011 19:04:56 -0400 Subject: [PATCH 02/77] Basic plucker working (text, non-composite images). --- src/calibre/ebooks/pdb/plucker/reader.py | 455 +++++++++++++++++++++-- 1 file changed, 425 insertions(+), 30 deletions(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index d1e5931580..502682baba 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -10,9 +10,13 @@ import os import struct import zlib +from collections import OrderedDict + from calibre import CurrentDir from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.pdb.formatreader import FormatReader +from calibre.ptempfile import TemporaryFile +from calibre.utils.magick import Image DATATYPE_PHTML = 0 DATATYPE_PHTML_COMPRESSED = 1 @@ -38,6 +42,100 @@ DATATYPE_EXT_ANCHOR_INDEX = 20 DATATYPE_EXT_ANCHOR = 21 DATATYPE_EXT_ANCHOR_COMPRESSED = 22 +# IETF IANA MIBenum value for the character set. +# See the http://www.iana.org/assignments/character-sets for valid values. +# Not all character sets are handled by Python. This is a small subset that +# the MIBenum maps to Python standard encodings +# from http://docs.python.org/library/codecs.html#standard-encodings +MIBNUM_TO_NAME = { + 3: 'ascii', + 4: 'latin_1', + 5: 'iso8859_2', + 6: 'iso8859_3', + 7: 'iso8859_4', + 8: 'iso8859_5', + 9: 'iso8859_6', + 10: 'iso8859_7', + 11: 'iso8859_8', + 12: 'iso8859_9', + 13: 'iso8859_10', + 17: 'shift_jis', + 18: 'euc_jp', + 27: 'utf_7', + 36: 'euc_kr', + 37: 'iso2022_kr', + 38: 'euc_kr', + 39: 'iso2022_jp', + 40: 'iso2022_jp_2', + 106: 'utf-8', + 109: 'iso8859_13', + 110: 'iso8859_14', + 111: 'iso8859_15', + 112: 'iso8859_16', + 1013: 'utf_16_be', + 1014: 'utf_16_le', + 1015: 'utf_16', + 2009: 'cp850', + 2010: 'cp852', + 2011: 'cp437', + 2013: 'cp862', + 2025: 'gb2312', + 2026: 'big5', + 2028: 'cp037', + 2043: 'cp424', + 2044: 'cp500', + 2046: 'cp855', + 2047: 'cp857', + 2048: 'cp860', + 2049: 'cp861', + 2050: 'cp863', + 2051: 'cp864', + 2052: 'cp865', + 2054: 'cp869', + 2063: 'cp1026', + 2085: 'hz', + 2086: 'cp866', + 2087: 'cp775', + 2089: 'cp858', + 2091: 'cp1140', + 2102: 'big5hkscs', + 2250: 'cp1250', + 2251: 'cp1251', + 2252: 'cp1252', + 2253: 'cp1253', + 2254: 'cp1254', + 2255: 'cp1255', + 2256: 'cp1256', + 2257: 'cp1257', + 2258: 'cp1258', +} + +def decompress_doc(data): + buffer = [ord(i) for i in data] + res = [] + i = 0 + while i < len(buffer): + c = buffer[i] + i += 1 + if c >= 1 and c <= 8: + res.extend(buffer[i:i+c]) + i += c + elif c <= 0x7f: + res.append(c) + elif c >= 0xc0: + res.extend( (ord(' '), c^0x80) ) + else: + c = (c << 8) + buffer[i] + i += 1 + di = (c & 0x3fff) >> 3 + j = len(res) + num = (c & ((1 << 3) - 1)) + 3 + + for k in range( num ): + res.append(res[j - di+k]) + + return ''.join([chr(i) for i in res]) + class HeaderRecord(object): def __init__(self, raw): @@ -68,14 +166,62 @@ class SectionHeader(object): class SectionHeaderText(object): - def __init__(self, data_header, raw): + def __init__(self, section_header, raw): self.sizes = [] self.attributes = [] - for i in xrange(data_header.paragraphs): + for i in xrange(section_header.paragraphs): adv = 4*i - self.sizes.append(struct.unpack('>H', raw[8+adv:10+adv])[0]) - self.attributes.append(struct.unpack('>H', raw[10+adv:12+adv])[0]) + self.sizes.append(struct.unpack('>H', raw[adv:2+adv])[0]) + self.attributes.append(struct.unpack('>H', raw[2+adv:4+adv])[0]) + +class SectionMetadata(object): + + def __init__(self, raw): + self.default_encoding = 'utf-8' + self.exceptional_uid_encodings = {} + self.owner_id = None + + record_count, = struct.unpack('>H', raw[0:2]) + + adv = 0 + for i in xrange(record_count): + type, = struct.unpack('>H', raw[2+adv:4+adv]) + length, = struct.unpack('>H', raw[4+adv:6+adv]) + + # CharSet + if type == 1: + val, = struct.unpack('>H', raw[6+adv:8+adv]) + self.default_encoding = MIBNUM_TO_NAME.get(val, 'utf-8') + # ExceptionalCharSets + elif type == 2: + ii_adv = 0 + for ii in xrange(length / 2): + uid, = struct.unpack('>H', raw[6+adv+ii_adv:8+adv+ii_adv]) + mib, = struct.unpack('>H', raw[8+adv+ii_adv:10+adv+ii_adv]) + self.exceptional_uid_encodings[uid] = MIBNUM_TO_NAME.get(mib, 'utf-8') + ii_adv += 4 + # OwnerID + elif type == 3: + self.owner_id = struct.unpack('>I', raw[6+adv:10+adv]) + # Author, Title, PubDate + # Ignored here. The metadata reader plugin + # will get this info because if it's missing + # the metadata reader plugin will use fall + # back data from elsewhere in the file. + elif type in (4, 5, 6): + pass + # Linked Documents + elif type == 7: + pass + + adv += 2*length + +class SectionText(object): + + def __init__(self, section_header, raw): + self.header = SectionHeaderText(section_header, raw) + self.data = raw[section_header.paragraphs * 4:] class Reader(FormatReader): @@ -84,53 +230,302 @@ class Reader(FormatReader): self.stream = stream self.log = log self.options = options - - self.sections = [] - for i in range(1, header.num_sections): - start = 8 - raw_data = header.section_data(i) - data_header = SectionHeader(raw_data) - sub_header = None - if data_header.type in (DATATYPE_PHTML, DATATYPE_PHTML_COMPRESSED): - sub_header = SectionHeaderText(data_header, raw_data) - start += data_header.paragraphs * 4 - self.sections.append((data_header, sub_header, raw_data[start:])) + # Mapping of section uid to our internal + # list of sections. + self.uid_section_number = OrderedDict() + self.uid_text_secion_number = OrderedDict() + self.uid_text_secion_encoding = {} + self.uid_image_section_number = {} + self.metadata_section_number = None + self.default_encoding = 'utf-8' + self.owner_id = None + self.sections = [] + self.header_record = HeaderRecord(header.section_data(0)) + + for i in range(1, header.num_sections): + section_number = i - 1 + start = 8 + section = None + + raw_data = header.section_data(i) + section_header = SectionHeader(raw_data) + + self.uid_section_number[section_header.uid] = section_number + + if section_header.type in (DATATYPE_PHTML, DATATYPE_PHTML_COMPRESSED): + self.uid_text_secion_number[section_header.uid] = section_number + section = SectionText(section_header, raw_data[start:]) + elif section_header.type in (DATATYPE_TBMP, DATATYPE_TBMP_COMPRESSED): + self.uid_image_section_number[section_header.uid] = section_number + section = raw_data[start:] + elif section_header.type == DATATYPE_METADATA: + self.metadata_section_number = section_number + section = SectionMetadata(raw_data[start:]) + elif section_header.type == DATATYPE_COMPOSITE_IMAGE: + + + self.sections.append((section_header, section)) + + if self.metadata_section_number: + mdata_section = self.sections[self.metadata_section_number][1] + for k, v in mdata_section.exceptional_uid_encodings.items(): + self.uid_text_secion_encoding[k] = v + self.default_encoding = mdata_section.default_encoding + self.owner_id = mdata_section.owner_id from calibre.ebooks.metadata.pdb import get_metadata self.mi = get_metadata(stream, False) def extract_content(self, output_dir): - html = u'' + html = u'' images = [] - - for header, sub_header, data in self.sections: - if header.type == DATATYPE_PHTML: - html += data - elif header.type == DATATYPE_PHTML_COMPRESSED: - d = self.decompress_phtml(data).decode('latin-1', 'replace') - print len(d) == header.size - html += d - - print html + + for uid, num in self.uid_text_secion_number.items(): + section_header, section_data = self.sections[num] + if section_header.type == DATATYPE_PHTML: + html += self.process_phtml(section_data.header, section_data.data.decode(self.get_text_uid_encoding(section_header.uid), 'replace')) + elif section_header.type == DATATYPE_PHTML_COMPRESSED: + d = self.decompress_phtml(section_data.data).decode(self.get_text_uid_encoding(section_header.uid), 'replace') + html += self.process_phtml(section_data.header, d) + + html += '' + with CurrentDir(output_dir): with open('index.html', 'wb') as index: self.log.debug('Writing text to index.html') index.write(html.encode('utf-8')) - + + if not os.path.exists(os.path.join(output_dir, 'images/')): + os.makedirs(os.path.join(output_dir, 'images/')) + with CurrentDir(os.path.join(output_dir, 'images/')): + #im.read('/Users/john/Tmp/plkr/apnx.palm') + for uid, num in self.uid_image_section_number.items(): + section_header, section_data = self.sections[num] + if section_data: + idata = None + if section_header.type == DATATYPE_TBMP: + idata = section_data + elif section_header.type == DATATYPE_TBMP_COMPRESSED: + if self.header_record.compression == 1: + idata = decompress_doc(section_data) + elif self.header_record.compression == 2: + idata = zlib.decompress(section_data) + try: + with TemporaryFile(suffix='.palm') as itn: + with open(itn, 'wb') as itf: + itf.write(idata) + im = Image() + im.read(itn) + im.set_compression_quality(70) + im.save('%s.jpg' % uid) + self.log.debug('Wrote image with uid %s to images/%s.jpg' % (uid, uid)) + except Exception as e: + self.log.error('Failed to write image with uid %s: %s' % (uid, e)) + images.append('%s.jpg' % uid) + else: + self.log.error('Failed to write image with uid %s: No data.' % uid) + opf_path = self.create_opf(output_dir, images) return opf_path def decompress_phtml(self, data): if self.header_record.compression == 2: - raise NotImplementedError - #return zlib.decompress(data) + if self.owner_id: + raise NotImplementedError + return zlib.decompress(data) elif self.header_record.compression == 1: - from calibre.ebooks.compression.palmdoc import decompress_doc + #from calibre.ebooks.compression.palmdoc import decompress_doc return decompress_doc(data) + def process_phtml(self, sub_header, d): + html = u'' + offset = 0 + paragraph_open = False + paragraph_offsets = [] + running_offset = 0 + for size in sub_header.sizes: + running_offset += size + paragraph_offsets.append(running_offset) + + while offset < len(d): + if not paragraph_open: + html += u'

' + paragraph_open = True + + c = ord(d[offset]) + if c == 0x0: + offset += 1 + c = ord(d[offset]) + # Page link begins + # 2 Bytes + # record ID + if c == 0x0a: + offset += 2 + # Targeted page link begins + # 3 Bytes + # record ID, target + elif c == 0x0b: + offset += 3 + # Paragraph link begins + # 4 Bytes + # record ID, paragraph number + elif c == 0x0c: + offset += 4 + # Targeted paragraph link begins + # 5 Bytes + # record ID, paragraph number, target + elif c == 0x0d: + offset += 5 + # Link ends + # 0 Bytes + elif c == 0x08: + pass + # Set font + # 1 Bytes + # font specifier + elif c == 0x11: + offset += 1 + # Embedded image + # 2 Bytes + # image record ID + elif c == 0x1a: + offset += 1 + uid = struct.unpack('>H', d[offset:offset+2])[0] + html += '' % uid + offset += 1 + # Set margin + # 2 Bytes + # left margin, right margin + elif c == 0x22: + offset += 2 + # Alignment of text + # 1 Bytes + # alignment + elif c == 0x29: + offset += 1 + # Horizontal rule + # 3 Bytes + # 8-bit height, 8-bit width (pixels), 8-bit width (%, 1-100) + elif c == 0x33: + offset += 3 + if paragraph_open: + html += u'

' + paragraph_open = False + html += u'
' + # New line + # 0 Bytes + elif c == 0x38: + if paragraph_open: + html += u'

\n' + paragraph_open = False + # Italic text begins + # 0 Bytes + elif c == 0x40: + html += u'' + # Italic text ends + # 0 Bytes + elif c == 0x48: + html += u'' + # Set text color + # 3 Bytes + # 8-bit red, 8-bit green, 8-bit blue + elif c == 0x53: + offset += 3 + # Multiple embedded image + # 4 Bytes + # alternate image record ID, image record ID + elif c == 0x5c: + offset += 4 + # Underline text begins + # 0 Bytes + elif c == 0x60: + html += u'' + # Underline text ends + # 0 Bytes + elif c == 0x68: + html += u'' + # Strike-through text begins + # 0 Bytes + elif c == 0x70: + html += u'' + # Strike-through text ends + # 0 Bytes + elif c == 0x78: + html += u'' + # 16-bit Unicode character + # 3 Bytes + # alternate text length, 16-bit unicode character + elif c == 0x83: + #offset += 2 + #c16 = d[offset:offset+2] + #html += c16.decode('utf-16') + #offset += 1 + offset += 3 + # 32-bit Unicode character + # 5 Bytes + # alternate text length, 32-bit unicode character + elif c == 0x85: + #offset += 2 + #c32 = d[offset:offset+4] + #html += c32.decode('utf-32') + #offset += 3 + offset += 5 + # Begin custom font span + # 6 Bytes + # font page record ID, X page position, Y page position + elif c == 0x8e: + offset += 6 + # Adjust custom font glyph position + # 4 Bytes + # X page position, Y page position + elif c == 0x8c: + offset += 4 + # Change font page + # 2 Bytes + # font record ID + elif c == 0x8a: + offset += 2 + # End custom font span + # 0 Bytes + elif c == 0x88: + pass + # Begin new table row + # 0 Bytes + elif c == 0x90: + pass + # Insert table (or table link) + # 2 Bytes + # table record ID + elif c == 0x92: + offset += 2 + # Table cell data + # 7 Bytes + # 8-bit alignment, 16-bit image record ID, 8-bit columns, 8-bit rows, 16-bit text length + elif c == 0x97: + offset += 7 + # Exact link modifier + # 2 Bytes + # Paragraph Offset (The Exact Link Modifier modifies a Paragraph Link or Targeted Paragraph Link function to specify an exact byte offset within the paragraph. This function must be followed immediately by the function it modifies). + elif c == 0x9a: + offset += 2 + else: + html += unichr(c) + offset += 1 + if offset in paragraph_offsets: + if paragraph_open: + html += u'

\n' + paragraph_open = False + + if paragraph_open: + html += u'

' + + return html + + def get_text_uid_encoding(self, uid): + return self.uid_text_secion_encoding.get(uid, self.default_encoding) def create_opf(self, output_dir, images): with CurrentDir(output_dir): From acaa06de53fe280084c753408b682df835b1cf2d Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 16 Apr 2011 14:13:45 -0400 Subject: [PATCH 03/77] Fix decoding text. Add internal link support. --- src/calibre/ebooks/pdb/plucker/reader.py | 43 +++++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 502682baba..13dea343a7 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -263,7 +263,7 @@ class Reader(FormatReader): elif section_header.type == DATATYPE_METADATA: self.metadata_section_number = section_number section = SectionMetadata(raw_data[start:]) - elif section_header.type == DATATYPE_COMPOSITE_IMAGE: + #elif section_header.type == DATATYPE_COMPOSITE_IMAGE: self.sections.append((section_header, section)) @@ -285,10 +285,10 @@ class Reader(FormatReader): for uid, num in self.uid_text_secion_number.items(): section_header, section_data = self.sections[num] if section_header.type == DATATYPE_PHTML: - html += self.process_phtml(section_data.header, section_data.data.decode(self.get_text_uid_encoding(section_header.uid), 'replace')) + html += self.process_phtml(section_data.header, section_data.data) elif section_header.type == DATATYPE_PHTML_COMPRESSED: - d = self.decompress_phtml(section_data.data).decode(self.get_text_uid_encoding(section_header.uid), 'replace') - html += self.process_phtml(section_data.header, d) + d = self.decompress_phtml(section_data.data) + html += self.process_phtml(section_header.uid, section_data.header, d).decode(self.get_text_uid_encoding(section_header.uid), 'replace') html += '' @@ -300,7 +300,6 @@ class Reader(FormatReader): if not os.path.exists(os.path.join(output_dir, 'images/')): os.makedirs(os.path.join(output_dir, 'images/')) with CurrentDir(os.path.join(output_dir, 'images/')): - #im.read('/Users/john/Tmp/plkr/apnx.palm') for uid, num in self.uid_image_section_number.items(): section_header, section_data = self.sections[num] if section_data: @@ -340,10 +339,12 @@ class Reader(FormatReader): #from calibre.ebooks.compression.palmdoc import decompress_doc return decompress_doc(data) - def process_phtml(self, sub_header, d): - html = u'' + def process_phtml(self, uid, sub_header, d): + html = u'

' % (uid, uid) offset = 0 - paragraph_open = False + paragraph_open = True + need_set_p_id = False + p_num = 1 paragraph_offsets = [] running_offset = 0 for size in sub_header.sizes: @@ -352,7 +353,12 @@ class Reader(FormatReader): while offset < len(d): if not paragraph_open: - html += u'

' + if need_set_p_id: + html += u'

' % (uid, p_num) + p_num += 1 + need_set_p_id = False + else: + html += u'

' paragraph_open = True c = ord(d[offset]) @@ -363,26 +369,36 @@ class Reader(FormatReader): # 2 Bytes # record ID if c == 0x0a: - offset += 2 + offset += 1 + id = struct.unpack('>H', d[offset:offset+2])[0] + html += '' % id + offset += 1 # Targeted page link begins # 3 Bytes # record ID, target elif c == 0x0b: offset += 3 + html += '' # Paragraph link begins # 4 Bytes # record ID, paragraph number elif c == 0x0c: - offset += 4 + offset += 1 + id = struct.unpack('>H', d[offset:offset+2])[0] + offset += 2 + pid = struct.unpack('>H', d[offset:offset+2])[0] + html += '' % (id, pid) + offset += 1 # Targeted paragraph link begins # 5 Bytes # record ID, paragraph number, target elif c == 0x0d: offset += 5 + html += '' # Link ends # 0 Bytes elif c == 0x08: - pass + html += '' # Set font # 1 Bytes # font specifier @@ -515,10 +531,11 @@ class Reader(FormatReader): html += unichr(c) offset += 1 if offset in paragraph_offsets: + need_set_p_id = True if paragraph_open: html += u'

\n' paragraph_open = False - + if paragraph_open: html += u'

' From 8557981a51d551907154684b7b16f4d89c56247b Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 16 Apr 2011 14:43:36 -0400 Subject: [PATCH 04/77] Don't put every PHTML record into one ordered html file. Plucker documents are groups of separate PHTML pages that are linked via hyperlinks. --- src/calibre/ebooks/pdb/plucker/reader.py | 78 ++++++++++++------------ 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 13dea343a7..171c051bbd 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -145,6 +145,7 @@ class HeaderRecord(object): # 1 is DOC compressed self.compression, = struct.unpack('>H', raw[2:4]) self.records, = struct.unpack('>H', raw[4:6]) + self.home_html = None self.reserved = {} for i in xrange(self.records): @@ -152,6 +153,8 @@ class HeaderRecord(object): name, = struct.unpack('>H', raw[6+adv:8+adv]) id, = struct.unpack('>H', raw[8+adv:10+adv]) self.reserved[id] = name + if name == 0: + self.home_html = id class SectionHeader(object): @@ -279,24 +282,21 @@ class Reader(FormatReader): self.mi = get_metadata(stream, False) def extract_content(self, output_dir): - html = u'' - images = [] - - for uid, num in self.uid_text_secion_number.items(): - section_header, section_data = self.sections[num] - if section_header.type == DATATYPE_PHTML: - html += self.process_phtml(section_data.header, section_data.data) - elif section_header.type == DATATYPE_PHTML_COMPRESSED: - d = self.decompress_phtml(section_data.data) - html += self.process_phtml(section_header.uid, section_data.header, d).decode(self.get_text_uid_encoding(section_header.uid), 'replace') - - html += '' - with CurrentDir(output_dir): - with open('index.html', 'wb') as index: - self.log.debug('Writing text to index.html') - index.write(html.encode('utf-8')) + for uid, num in self.uid_text_secion_number.items(): + self.log.debug(_('Writing record with uid: %s as %s.html' % (uid, uid))) + with open('%s.html' % uid, 'wb') as htmlf: + html = u'' + section_header, section_data = self.sections[num] + if section_header.type == DATATYPE_PHTML: + html += self.process_phtml(section_data.header, section_data.data) + elif section_header.type == DATATYPE_PHTML_COMPRESSED: + d = self.decompress_phtml(section_data.data) + html += self.process_phtml(section_data.header, d).decode(self.get_text_uid_encoding(section_header.uid), 'replace') + html += '' + htmlf.write(html.encode('utf-8')) + images = [] if not os.path.exists(os.path.join(output_dir, 'images/')): os.makedirs(os.path.join(output_dir, 'images/')) with CurrentDir(os.path.join(output_dir, 'images/')): @@ -326,9 +326,25 @@ class Reader(FormatReader): else: self.log.error('Failed to write image with uid %s: No data.' % uid) - opf_path = self.create_opf(output_dir, images) + # Run the HTML through the html processing plugin. + from calibre.customize.ui import plugin_for_input_format + html_input = plugin_for_input_format('html') + for opt in html_input.options: + setattr(self.options, opt.option.name, opt.recommended_value) + self.options.input_encoding = 'utf-8' + odi = self.options.debug_pipeline + self.options.debug_pipeline = None + # Generate oeb from html conversion. + try: + home_html = self.header_record.home_html + if not home_html: + home_html = self.uid_text_secion_number.items()[0][0] + except: + raise Exception(_('Could not determine home.html')) + oeb = html_input.convert(open('%s.html' % home_html, 'rb'), self.options, 'html', self.log, {}) + self.options.debug_pipeline = odi - return opf_path + return oeb def decompress_phtml(self, data): if self.header_record.compression == 2: @@ -339,8 +355,8 @@ class Reader(FormatReader): #from calibre.ebooks.compression.palmdoc import decompress_doc return decompress_doc(data) - def process_phtml(self, uid, sub_header, d): - html = u'

' % (uid, uid) + def process_phtml(self, sub_header, d): + html = u'

' offset = 0 paragraph_open = True need_set_p_id = False @@ -354,7 +370,7 @@ class Reader(FormatReader): while offset < len(d): if not paragraph_open: if need_set_p_id: - html += u'

' % (uid, p_num) + html += u'

' % p_num p_num += 1 need_set_p_id = False else: @@ -371,7 +387,7 @@ class Reader(FormatReader): if c == 0x0a: offset += 1 id = struct.unpack('>H', d[offset:offset+2])[0] - html += '' % id + html += '' % id offset += 1 # Targeted page link begins # 3 Bytes @@ -387,7 +403,7 @@ class Reader(FormatReader): id = struct.unpack('>H', d[offset:offset+2])[0] offset += 2 pid = struct.unpack('>H', d[offset:offset+2])[0] - html += '' % (id, pid) + html += '' % (id, pid) offset += 1 # Targeted paragraph link begins # 5 Bytes @@ -543,19 +559,3 @@ class Reader(FormatReader): def get_text_uid_encoding(self, uid): return self.uid_text_secion_encoding.get(uid, self.default_encoding) - - def create_opf(self, output_dir, images): - with CurrentDir(output_dir): - opf = OPFCreator(output_dir, self.mi) - - manifest = [('index.html', None)] - - for i in images: - manifest.append((os.path.join('images/', i), None)) - - opf.create_manifest(manifest) - opf.create_spine(['index.html']) - with open('metadata.opf', 'wb') as opffile: - opf.render(opffile) - - return os.path.join(output_dir, 'metadata.opf') From 644335d97b1494ee04c3c657d435a3aeef44551c Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 16 Apr 2011 21:23:13 -0400 Subject: [PATCH 05/77] Ignore non internal links. Support composite images. --- src/calibre/ebooks/pdb/plucker/reader.py | 109 ++++++++++++++++++++--- 1 file changed, 99 insertions(+), 10 deletions(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 171c051bbd..c6c404b125 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -13,10 +13,9 @@ import zlib from collections import OrderedDict from calibre import CurrentDir -from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.pdb.formatreader import FormatReader from calibre.ptempfile import TemporaryFile -from calibre.utils.magick import Image +from calibre.utils.magick import Image, create_canvas DATATYPE_PHTML = 0 DATATYPE_PHTML_COMPRESSED = 1 @@ -178,6 +177,7 @@ class SectionHeaderText(object): self.sizes.append(struct.unpack('>H', raw[adv:2+adv])[0]) self.attributes.append(struct.unpack('>H', raw[2+adv:4+adv])[0]) + class SectionMetadata(object): def __init__(self, raw): @@ -220,6 +220,7 @@ class SectionMetadata(object): adv += 2*length + class SectionText(object): def __init__(self, section_header, raw): @@ -227,6 +228,34 @@ class SectionText(object): self.data = raw[section_header.paragraphs * 4:] +class SectionCompositeImage(object): + + def __init__(self, raw): + self.columns, = struct.unpack('>H', raw[0:2]) + self.rows, = struct.unpack('>H', raw[2:4]) + + # [ + # row [col, col, col...], + # row [col, col, col...], + # ... + # ] + # + # Each item in the layout is in it's + # correct position in the final + # composite. + # + # Each item in the layout is a uid + # to an image record. + self.layout = [] + offset = 4 + for i in xrange(self.rows): + col = [] + for j in xrange(self.columns): + col.append(struct.unpack('>H', raw[offset:offset+2])[0]) + offset += 2 + self.layout.append(col) + + class Reader(FormatReader): def __init__(self, header, stream, log, options): @@ -240,6 +269,7 @@ class Reader(FormatReader): self.uid_text_secion_number = OrderedDict() self.uid_text_secion_encoding = {} self.uid_image_section_number = {} + self.uid_composite_image_section_number = {} self.metadata_section_number = None self.default_encoding = 'utf-8' self.owner_id = None @@ -266,8 +296,9 @@ class Reader(FormatReader): elif section_header.type == DATATYPE_METADATA: self.metadata_section_number = section_number section = SectionMetadata(raw_data[start:]) - #elif section_header.type == DATATYPE_COMPOSITE_IMAGE: - + elif section_header.type == DATATYPE_COMPOSITE_IMAGE: + self.uid_composite_image_section_number[section_header.uid] = section_number + section = SectionCompositeImage(raw_data[start:]) self.sections.append((section_header, section)) @@ -282,6 +313,9 @@ class Reader(FormatReader): self.mi = get_metadata(stream, False) def extract_content(self, output_dir): + # Each text record is independent (unless the continuation + # value is set in the previous record). Put each converted + # text recored into a separate file. with CurrentDir(output_dir): for uid, num in self.uid_text_secion_number.items(): self.log.debug(_('Writing record with uid: %s as %s.html' % (uid, uid))) @@ -297,9 +331,11 @@ class Reader(FormatReader): htmlf.write(html.encode('utf-8')) images = [] + image_sizes = {} if not os.path.exists(os.path.join(output_dir, 'images/')): os.makedirs(os.path.join(output_dir, 'images/')) with CurrentDir(os.path.join(output_dir, 'images/')): + # Single images. for uid, num in self.uid_image_section_number.items(): section_header, section_data = self.sections[num] if section_data: @@ -317,6 +353,7 @@ class Reader(FormatReader): itf.write(idata) im = Image() im.read(itn) + image_sizes[uid] = im.size im.set_compression_quality(70) im.save('%s.jpg' % uid) self.log.debug('Wrote image with uid %s to images/%s.jpg' % (uid, uid)) @@ -325,6 +362,49 @@ class Reader(FormatReader): images.append('%s.jpg' % uid) else: self.log.error('Failed to write image with uid %s: No data.' % uid) + # Composite images. + for uid, num in self.uid_composite_image_section_number.items(): + try: + section_header, section_data = self.sections[num] + # Get the final width and height. + width = 0 + height = 0 + for row in section_data.layout: + row_width = 0 + col_height = 0 + for col in row: + if col not in image_sizes: + raise Exception('Image with uid: %s missing.' % col) + im = Image() + im.read('%s.jpg' % col) + w, h = im.size + row_width += w + if col_height < h: + col_height = h + if width < row_width: + width = row_width + height += col_height + # Create a new image the total size of all image + # parts. Put the parts into the new image. + canvas = create_canvas(width, height) + y_off = 0 + for row in section_data.layout: + x_off = 0 + largest_height = 0 + for col in row: + im = Image() + im.read('%s.jpg' % col) + canvas.compose(im, x_off, y_off) + w, h = im.size + x_off += w + if largest_height < h: + largest_height = h + y_off += largest_height + canvas.set_compression_quality(70) + canvas.save('%s.jpg' % uid) + self.log.debug('Wrote composite image with uid %s to images/%s.jpg' % (uid, uid)) + except Exception as e: + self.log.error('Failed to write composite image with uid %s: %s' % (uid, e)) # Run the HTML through the html processing plugin. from calibre.customize.ui import plugin_for_input_format @@ -334,13 +414,17 @@ class Reader(FormatReader): self.options.input_encoding = 'utf-8' odi = self.options.debug_pipeline self.options.debug_pipeline = None - # Generate oeb from html conversion. + # Determine the home.html record uid. This should be set in the + # reserved values in the metadata recored. home.html is the first + # text record (should have hyper link references to other records) + # in the document. try: home_html = self.header_record.home_html if not home_html: home_html = self.uid_text_secion_number.items()[0][0] except: raise Exception(_('Could not determine home.html')) + # Generate oeb from html conversion. oeb = html_input.convert(open('%s.html' % home_html, 'rb'), self.options, 'html', self.log, {}) self.options.debug_pipeline = odi @@ -359,6 +443,7 @@ class Reader(FormatReader): html = u'

' offset = 0 paragraph_open = True + link_open = False need_set_p_id = False p_num = 1 paragraph_offsets = [] @@ -387,14 +472,15 @@ class Reader(FormatReader): if c == 0x0a: offset += 1 id = struct.unpack('>H', d[offset:offset+2])[0] - html += '' % id + if id in self.uid_text_secion_number: + html += '' % id + link_open = True offset += 1 # Targeted page link begins # 3 Bytes # record ID, target elif c == 0x0b: offset += 3 - html += '' # Paragraph link begins # 4 Bytes # record ID, paragraph number @@ -403,18 +489,21 @@ class Reader(FormatReader): id = struct.unpack('>H', d[offset:offset+2])[0] offset += 2 pid = struct.unpack('>H', d[offset:offset+2])[0] - html += '' % (id, pid) + if id in self.uid_text_secion_number: + html += '' % (id, pid) + link_open = True offset += 1 # Targeted paragraph link begins # 5 Bytes # record ID, paragraph number, target elif c == 0x0d: offset += 5 - html += '' # Link ends # 0 Bytes elif c == 0x08: - html += '' + if link_open: + html += '' + link_open = False # Set font # 1 Bytes # font specifier From 494c040d36b2ee5620143f5bf70600a811d2de1e Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 16 Apr 2011 21:54:28 -0400 Subject: [PATCH 06/77] Comments. --- src/calibre/ebooks/pdb/plucker/reader.py | 74 ++++++++++++++++++++---- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index c6c404b125..20943be3f0 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -136,6 +136,9 @@ def decompress_doc(data): return ''.join([chr(i) for i in res]) class HeaderRecord(object): + ''' + Plucker header. PDB record 0. + ''' def __init__(self, raw): self.uid, = struct.unpack('>H', raw[0:2]) @@ -144,6 +147,8 @@ class HeaderRecord(object): # 1 is DOC compressed self.compression, = struct.unpack('>H', raw[2:4]) self.records, = struct.unpack('>H', raw[4:6]) + # uid of the first html file. This should link + # to other files which in turn may link to others. self.home_html = None self.reserved = {} @@ -157,6 +162,10 @@ class HeaderRecord(object): class SectionHeader(object): + ''' + Every sections (record) has this header. It gives + details about the section such as it's uid. + ''' def __init__(self, raw): self.uid, = struct.unpack('>H', raw[0:2]) @@ -167,9 +176,14 @@ class SectionHeader(object): class SectionHeaderText(object): + ''' + Sub header for text records. + ''' def __init__(self, section_header, raw): + # The uncompressed size of each paragraph. self.sizes = [] + # Paragraph attributes. self.attributes = [] for i in xrange(section_header.paragraphs): @@ -179,6 +193,19 @@ class SectionHeaderText(object): class SectionMetadata(object): + ''' + Metadata. + + This does not store metadata such as title, or author. + That metadata would be best retrieved with the PDB (plucker) + metdata reader. + + This stores document specific information such as the + text encoding. + + Note: There is a default encoding but each text section + can be assigned a different encoding. + ''' def __init__(self, raw): self.default_encoding = 'utf-8' @@ -222,6 +249,9 @@ class SectionMetadata(object): class SectionText(object): + ''' + Text data. Stores a text section header and the PHTML. + ''' def __init__(self, section_header, raw): self.header = SectionHeaderText(section_header, raw) @@ -229,14 +259,19 @@ class SectionText(object): class SectionCompositeImage(object): + ''' + A composite image consists of a a 2D array + of rows and columns. The entries in the array + are uid's. + ''' def __init__(self, raw): self.columns, = struct.unpack('>H', raw[0:2]) self.rows, = struct.unpack('>H', raw[2:4]) # [ - # row [col, col, col...], - # row [col, col, col...], + # [uid, uid, uid, ...], + # [uid, uid, uid, ...], # ... # ] # @@ -275,18 +310,21 @@ class Reader(FormatReader): self.owner_id = None self.sections = [] + # The Plucker record0 header self.header_record = HeaderRecord(header.section_data(0)) for i in range(1, header.num_sections): - section_number = i - 1 + section_number = len(self.sections) + # The length of the section header. + # Where the actual data in the section starts. start = 8 section = None raw_data = header.section_data(i) + # Every sections has a section header. section_header = SectionHeader(raw_data) - - self.uid_section_number[section_header.uid] = section_number - + + # Store sections we care able. if section_header.type in (DATATYPE_PHTML, DATATYPE_PHTML_COMPRESSED): self.uid_text_secion_number[section_header.uid] = section_number section = SectionText(section_header, raw_data[start:]) @@ -300,8 +338,13 @@ class Reader(FormatReader): self.uid_composite_image_section_number[section_header.uid] = section_number section = SectionCompositeImage(raw_data[start:]) - self.sections.append((section_header, section)) + # Store the section. + if section: + self.uid_section_number[section_header.uid] = section_number + self.sections.append((section_header, section)) + # Store useful information from the metadata section locally + # to make access easier. if self.metadata_section_number: mdata_section = self.sections[self.metadata_section_number][1] for k, v in mdata_section.exceptional_uid_encodings.items(): @@ -309,13 +352,16 @@ class Reader(FormatReader): self.default_encoding = mdata_section.default_encoding self.owner_id = mdata_section.owner_id + # Get the metadata (tile, author, ...) with the metadata reader. from calibre.ebooks.metadata.pdb import get_metadata self.mi = get_metadata(stream, False) def extract_content(self, output_dir): # Each text record is independent (unless the continuation # value is set in the previous record). Put each converted - # text recored into a separate file. + # text recored into a separate file. We will reference the + # home.html file as the first file and let the HTML input + # plugin assemble the order based on hyperlinks. with CurrentDir(output_dir): for uid, num in self.uid_text_secion_number.items(): self.log.debug(_('Writing record with uid: %s as %s.html' % (uid, uid))) @@ -329,8 +375,9 @@ class Reader(FormatReader): html += self.process_phtml(section_data.header, d).decode(self.get_text_uid_encoding(section_header.uid), 'replace') html += '' htmlf.write(html.encode('utf-8')) - - images = [] + + # Images. + # Cache the image sizes in case they are used by a composite image. image_sizes = {} if not os.path.exists(os.path.join(output_dir, 'images/')): os.makedirs(os.path.join(output_dir, 'images/')) @@ -359,10 +406,10 @@ class Reader(FormatReader): self.log.debug('Wrote image with uid %s to images/%s.jpg' % (uid, uid)) except Exception as e: self.log.error('Failed to write image with uid %s: %s' % (uid, e)) - images.append('%s.jpg' % uid) else: self.log.error('Failed to write image with uid %s: No data.' % uid) # Composite images. + # We're going to use the already compressed .jpg images here. for uid, num in self.uid_composite_image_section_number.items(): try: section_header, section_data = self.sections[num] @@ -559,7 +606,10 @@ class Reader(FormatReader): # 4 Bytes # alternate image record ID, image record ID elif c == 0x5c: - offset += 4 + offset += 3 + uid = struct.unpack('>H', d[offset:offset+2])[0] + html += '' % uid + offset += 1 # Underline text begins # 0 Bytes elif c == 0x60: From 93492a9ec8f01233723bd8b6038a0440c738a705 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 17 Apr 2011 09:42:00 -0400 Subject: [PATCH 07/77] Add font changes. --- src/calibre/ebooks/pdb/plucker/reader.py | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 20943be3f0..5c128fa3d3 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -493,6 +493,7 @@ class Reader(FormatReader): link_open = False need_set_p_id = False p_num = 1 + font_specifier_close = '' paragraph_offsets = [] running_offset = 0 for size in sub_header.sizes: @@ -556,6 +557,55 @@ class Reader(FormatReader): # font specifier elif c == 0x11: offset += 1 + specifier = d[offset] + html += font_specifier_close + # Regular text + if specifier == 0: + font_specifier_close = '' + # h1 + elif specifier == 1: + html += '

' + font_specifier_close = '

' + # h2 + elif specifier == 2: + html += '

' + font_specifier_close = '

' + # h3 + elif specifier == 3: + html += '' + font_specifier_close = '' + # h4 + elif specifier == 4: + html += '

' + font_specifier_close = '

' + # h5 + elif specifier == 5: + html += '
' + font_specifier_close = '
' + # h6 + elif specifier == 6: + html += '
' + font_specifier_close = '
' + # Bold + elif specifier == 7: + html += '' + font_specifier_close = '' + # Fixed-width + elif specifier == 8: + html += '' + font_specifier_close = '' + # Small + elif specifier == 9: + html += '' + font_specifier_close = '' + # Subscript + elif specifier == 10: + html += '' + font_specifier_close = '' + # Superscript + elif specifier == 11: + html += '' + font_specifier_close = '' # Embedded image # 2 Bytes # image record ID From 87bb34d9940a39553d4f72a056486cb20a88e587 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 17 Apr 2011 10:03:50 -0400 Subject: [PATCH 08/77] Use latin-1 instead of utf-8 for default encoding. --- src/calibre/ebooks/pdb/plucker/reader.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 5c128fa3d3..9ae449e579 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -208,7 +208,7 @@ class SectionMetadata(object): ''' def __init__(self, raw): - self.default_encoding = 'utf-8' + self.default_encoding = 'latin-1' self.exceptional_uid_encodings = {} self.owner_id = None @@ -222,14 +222,14 @@ class SectionMetadata(object): # CharSet if type == 1: val, = struct.unpack('>H', raw[6+adv:8+adv]) - self.default_encoding = MIBNUM_TO_NAME.get(val, 'utf-8') + self.default_encoding = MIBNUM_TO_NAME.get(val, 'latin-1') # ExceptionalCharSets elif type == 2: ii_adv = 0 for ii in xrange(length / 2): uid, = struct.unpack('>H', raw[6+adv+ii_adv:8+adv+ii_adv]) mib, = struct.unpack('>H', raw[8+adv+ii_adv:10+adv+ii_adv]) - self.exceptional_uid_encodings[uid] = MIBNUM_TO_NAME.get(mib, 'utf-8') + self.exceptional_uid_encodings[uid] = MIBNUM_TO_NAME.get(mib, 'latin-1') ii_adv += 4 # OwnerID elif type == 3: @@ -306,7 +306,7 @@ class Reader(FormatReader): self.uid_image_section_number = {} self.uid_composite_image_section_number = {} self.metadata_section_number = None - self.default_encoding = 'utf-8' + self.default_encoding = 'latin-1' self.owner_id = None self.sections = [] @@ -680,10 +680,12 @@ class Reader(FormatReader): # 3 Bytes # alternate text length, 16-bit unicode character elif c == 0x83: - #offset += 2 + #offset += 1 + #alt_len = struct.unpack('>B', str(d[offset]))[0] + #offset += 1 #c16 = d[offset:offset+2] #html += c16.decode('utf-16') - #offset += 1 + #offset += 1 + alt_len offset += 3 # 32-bit Unicode character # 5 Bytes From 15a0384481e3e8ec9ee3adce0b082c21bde51fd2 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 17 Apr 2011 10:59:15 -0400 Subject: [PATCH 09/77] .. --- src/calibre/ebooks/pdb/plucker/reader.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 9ae449e579..ced9dafc0f 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -680,21 +680,11 @@ class Reader(FormatReader): # 3 Bytes # alternate text length, 16-bit unicode character elif c == 0x83: - #offset += 1 - #alt_len = struct.unpack('>B', str(d[offset]))[0] - #offset += 1 - #c16 = d[offset:offset+2] - #html += c16.decode('utf-16') - #offset += 1 + alt_len offset += 3 # 32-bit Unicode character # 5 Bytes # alternate text length, 32-bit unicode character elif c == 0x85: - #offset += 2 - #c32 = d[offset:offset+4] - #html += c32.decode('utf-32') - #offset += 3 offset += 5 # Begin custom font span # 6 Bytes From 05fc3eec93fd3b05af981bc3d20a1627673aa043 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 17 Apr 2011 11:09:46 -0400 Subject: [PATCH 10/77] Add todo for non supported features. --- src/calibre/ebooks/pdb/plucker/reader.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index ced9dafc0f..207a466178 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -292,6 +292,18 @@ class SectionCompositeImage(object): class Reader(FormatReader): + ''' + Convert a plucker archive into HTML. + + TODO: + * UTF 16 and 32 characters. + * Margins. + * Alignment. + * DATATYPE_MAILTO + * DATATYPE_TABLE(_COMPRESSED) + * DATATYPE_EXT_ANCHOR_INDEX + * DATATYPE_EXT_ANCHOR(_COMPRESSED) + ''' def __init__(self, header, stream, log, options): self.stream = stream From c0cf0e91d47b1213b2093bac4cdd1317a87b258f Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 17 Apr 2011 19:28:04 -0400 Subject: [PATCH 11/77] Allow user specify input encoding and override what is specified by the file. Turn 0xa0 character into nbsp entity. --- src/calibre/ebooks/pdb/plucker/reader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 207a466178..5fa66e1246 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -523,6 +523,7 @@ class Reader(FormatReader): paragraph_open = True c = ord(d[offset]) + # PHTML "functions" if c == 0x0: offset += 1 c = ord(d[offset]) @@ -736,6 +737,8 @@ class Reader(FormatReader): # Paragraph Offset (The Exact Link Modifier modifies a Paragraph Link or Targeted Paragraph Link function to specify an exact byte offset within the paragraph. This function must be followed immediately by the function it modifies). elif c == 0x9a: offset += 2 + elif c == 0xa0: + html += ' ' else: html += unichr(c) offset += 1 @@ -751,4 +754,4 @@ class Reader(FormatReader): return html def get_text_uid_encoding(self, uid): - return self.uid_text_secion_encoding.get(uid, self.default_encoding) + return self.options.input_encoding if self.options.input_encoding else self.uid_text_secion_encoding.get(uid, self.default_encoding) From 377313df7d4343ab9e4035877ac2d184f5dd73ba Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 17 Apr 2011 19:42:39 -0400 Subject: [PATCH 12/77] cleanup. --- src/calibre/ebooks/pdb/plucker/reader.py | 25 +++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 5fa66e1246..9f1d2ad426 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -183,6 +183,9 @@ class SectionHeaderText(object): def __init__(self, section_header, raw): # The uncompressed size of each paragraph. self.sizes = [] + # uncompressed offset of each paragraph starting + # at the beginning of the PHTML. + self.paragraph_offsets = [] # Paragraph attributes. self.attributes = [] @@ -191,6 +194,11 @@ class SectionHeaderText(object): self.sizes.append(struct.unpack('>H', raw[adv:2+adv])[0]) self.attributes.append(struct.unpack('>H', raw[2+adv:4+adv])[0]) + running_offset = 0 + for size in self.sizes: + running_offset += size + self.paragraph_offsets.append(running_offset) + class SectionMetadata(object): ''' @@ -299,6 +307,7 @@ class Reader(FormatReader): * UTF 16 and 32 characters. * Margins. * Alignment. + * Font color. * DATATYPE_MAILTO * DATATYPE_TABLE(_COMPRESSED) * DATATYPE_EXT_ANCHOR_INDEX @@ -381,13 +390,13 @@ class Reader(FormatReader): html = u'' section_header, section_data = self.sections[num] if section_header.type == DATATYPE_PHTML: - html += self.process_phtml(section_data.header, section_data.data) + html += self.process_phtml(section_data.data, section_data.header.paragraph_offsets) elif section_header.type == DATATYPE_PHTML_COMPRESSED: d = self.decompress_phtml(section_data.data) - html += self.process_phtml(section_data.header, d).decode(self.get_text_uid_encoding(section_header.uid), 'replace') + html += self.process_phtml(d, section_data.header.paragraph_offsets).decode(self.get_text_uid_encoding(section_header.uid), 'replace') html += '' htmlf.write(html.encode('utf-8')) - + # Images. # Cache the image sizes in case they are used by a composite image. image_sizes = {} @@ -498,7 +507,7 @@ class Reader(FormatReader): #from calibre.ebooks.compression.palmdoc import decompress_doc return decompress_doc(data) - def process_phtml(self, sub_header, d): + def process_phtml(self, d, paragraph_offsets=[]): html = u'

' offset = 0 paragraph_open = True @@ -506,11 +515,6 @@ class Reader(FormatReader): need_set_p_id = False p_num = 1 font_specifier_close = '' - paragraph_offsets = [] - running_offset = 0 - for size in sub_header.sizes: - running_offset += size - paragraph_offsets.append(running_offset) while offset < len(d): if not paragraph_open: @@ -754,4 +758,7 @@ class Reader(FormatReader): return html def get_text_uid_encoding(self, uid): + # Return the user sepcified input encoding, + # otherwise return the alternate encoding specified for the uid, + # otherwise retur the default encoding for the document. return self.options.input_encoding if self.options.input_encoding else self.uid_text_secion_encoding.get(uid, self.default_encoding) From 57b3531051d291e179861ff54e089559230246d6 Mon Sep 17 00:00:00 2001 From: John Schember Date: Fri, 22 Apr 2011 20:04:50 -0400 Subject: [PATCH 13/77] Store: Simplify threads. Tie all threads to progress indicator. --- .../gui2/store/search/download_thread.py | 90 ++++++++++--------- src/calibre/gui2/store/search/models.py | 10 +-- src/calibre/gui2/store/search/search.py | 21 +++-- 3 files changed, 65 insertions(+), 56 deletions(-) diff --git a/src/calibre/gui2/store/search/download_thread.py b/src/calibre/gui2/store/search/download_thread.py index a6f92011f6..4dd3c4a59b 100644 --- a/src/calibre/gui2/store/search/download_thread.py +++ b/src/calibre/gui2/store/search/download_thread.py @@ -6,7 +6,6 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -import time import traceback from contextlib import closing from threading import Thread @@ -17,7 +16,9 @@ from calibre.utils.magick.draw import thumbnail class GenericDownloadThreadPool(object): ''' - add_task must be implemented in a subclass. + add_task must be implemented in a subclass and must + GenericDownloadThreadPool.add_task must be called + at the end of the function. ''' def __init__(self, thread_type, thread_count): @@ -29,10 +30,16 @@ class GenericDownloadThreadPool(object): self.threads = [] def add_task(self): - raise NotImplementedError() - - def start_threads(self): - for i in range(self.thread_count): + ''' + This must be implemented in a sub class and this function + must be called at the end of the add_task function in + the sub class. + + The implementation of this function (in this base class) + starts any threads necessary to fill the pool if it is + not already full. + ''' + for i in xrange(self.thread_count - self.running_threads_count()): t = self.thread_type(self.tasks, self.results) self.threads.append(t) t.start() @@ -60,10 +67,14 @@ class GenericDownloadThreadPool(object): return not self.results.empty() def threads_running(self): + return self.running_threads_count() > 0 + + def running_threads_count(self): + count = 0 for t in self.threads: if t.is_alive(): - return True - return False + count += 1 + return count class SearchThreadPool(GenericDownloadThreadPool): @@ -73,17 +84,16 @@ class SearchThreadPool(GenericDownloadThreadPool): using start_threads(). Reset by calling abort(). Example: - sp = SearchThreadPool(SearchThread, 3) - add tasks using add_task(...) - sp.start_threads() - all threads have finished. - sp.abort() - add tasks using add_task(...) - sp.start_threads() + sp = SearchThreadPool(3) + sp.add_task(...) ''' + + def __init__(self, thread_count): + GenericDownloadThreadPool.__init__(self, SearchThread, thread_count) def add_task(self, query, store_name, store_plugin, timeout): self.tasks.put((query, store_name, store_plugin, timeout)) + GenericDownloadThreadPool.add_task(self) class SearchThread(Thread): @@ -113,12 +123,13 @@ class SearchThread(Thread): class CoverThreadPool(GenericDownloadThreadPool): - ''' - Once started all threads run until abort is called. - ''' + + def __init__(self, thread_count): + GenericDownloadThreadPool.__init__(self, CoverThread, thread_count) def add_task(self, search_result, update_callback, timeout=5): self.tasks.put((search_result, update_callback, timeout)) + GenericDownloadThreadPool.add_task(self) class CoverThread(Thread): @@ -136,30 +147,27 @@ class CoverThread(Thread): self._run = False def run(self): - while self._run: + while self._run and not self.tasks.empty(): try: - time.sleep(.1) - while not self.tasks.empty(): - if not self._run: - break - result, callback, timeout = self.tasks.get() - if result and result.cover_url: - with closing(self.br.open(result.cover_url, timeout=timeout)) as f: - result.cover_data = f.read() - result.cover_data = thumbnail(result.cover_data, 64, 64)[2] - callback() - self.tasks.task_done() + result, callback, timeout = self.tasks.get() + if result and result.cover_url: + with closing(self.br.open(result.cover_url, timeout=timeout)) as f: + result.cover_data = f.read() + result.cover_data = thumbnail(result.cover_data, 64, 64)[2] + callback() + self.tasks.task_done() except: continue class DetailsThreadPool(GenericDownloadThreadPool): - ''' - Once started all threads run until abort is called. - ''' + + def __init__(self, thread_count): + GenericDownloadThreadPool.__init__(self, DetailsThread, thread_count) def add_task(self, search_result, store_plugin, update_callback, timeout=10): self.tasks.put((search_result, store_plugin, update_callback, timeout)) + GenericDownloadThreadPool.add_task(self) class DetailsThread(Thread): @@ -175,16 +183,12 @@ class DetailsThread(Thread): self._run = False def run(self): - while self._run: + while self._run and not self.tasks.empty(): try: - time.sleep(.1) - while not self.tasks.empty(): - if not self._run: - break - result, store_plugin, callback, timeout = self.tasks.get() - if result: - store_plugin.get_details(result, timeout) - callback(result) - self.tasks.task_done() + result, store_plugin, callback, timeout = self.tasks.get() + if result: + store_plugin.get_details(result, timeout) + callback(result) + self.tasks.task_done() except: continue diff --git a/src/calibre/gui2/store/search/models.py b/src/calibre/gui2/store/search/models.py index 73b7bcc90a..adc90e3b14 100644 --- a/src/calibre/gui2/store/search/models.py +++ b/src/calibre/gui2/store/search/models.py @@ -14,7 +14,7 @@ from PyQt4.Qt import (Qt, QAbstractItemModel, QVariant, QPixmap, QModelIndex, QS from calibre.gui2 import NONE from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.search.download_thread import DetailsThreadPool, \ - DetailsThread, CoverThreadPool, CoverThread + CoverThreadPool from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ REGEXP_MATCH from calibre.utils.icu import sort_key @@ -51,10 +51,8 @@ class Matches(QAbstractItemModel): self.matches = [] self.query = '' self.search_filter = SearchFilter() - self.cover_pool = CoverThreadPool(CoverThread, 2) - self.cover_pool.start_threads() - self.details_pool = DetailsThreadPool(DetailsThread, 4) - self.details_pool.start_threads() + self.cover_pool = CoverThreadPool(2) + self.details_pool = DetailsThreadPool(4) self.sort_col = 2 self.sort_order = Qt.AscendingOrder @@ -70,9 +68,7 @@ class Matches(QAbstractItemModel): self.search_filter.clear_search_results() self.query = '' self.cover_pool.abort() - self.cover_pool.start_threads() self.details_pool.abort() - self.details_pool.start_threads() self.reset() def add_result(self, result, store_plugin): diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 5c4b1cee00..8c3cf8452e 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -13,7 +13,7 @@ from PyQt4.Qt import (Qt, QDialog, QTimer, QCheckBox, QVBoxLayout) from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator -from calibre.gui2.store.search.download_thread import SearchThreadPool, SearchThread +from calibre.gui2.store.search.download_thread import SearchThreadPool from calibre.gui2.store.search.search_ui import Ui_Dialog HANG_TIME = 75000 # milliseconds seconds @@ -31,9 +31,10 @@ class SearchDialog(QDialog, Ui_Dialog): # We keep a cache of store plugins and reference them by name. self.store_plugins = istores - self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL) + self.search_pool = SearchThreadPool(SEARCH_THREAD_TOTAL) # Check for results and hung threads. self.checker = QTimer() + self.progress_checker = QTimer() self.hang_check = 0 # Add check boxes for each store so the user @@ -54,12 +55,15 @@ class SearchDialog(QDialog, Ui_Dialog): self.search.clicked.connect(self.do_search) self.checker.timeout.connect(self.get_results) + self.progress_checker.timeout.connect(self.check_progress) self.results_view.activated.connect(self.open_store) self.select_all_stores.clicked.connect(self.stores_select_all) self.select_invert_stores.clicked.connect(self.stores_select_invert) self.select_none_stores.clicked.connect(self.stores_select_none) self.finished.connect(self.dialog_closed) + self.progress_checker.start(100) + self.restore_state() def resize_columns(self): @@ -105,10 +109,9 @@ class SearchDialog(QDialog, Ui_Dialog): for n in store_names: if getattr(self, 'store_check_' + n).isChecked(): self.search_pool.add_task(query, n, self.store_plugins[n], TIMEOUT) - if self.search_pool.has_tasks(): + if self.search_pool.has_tasks() or self.search_pool.threads_running(): self.hang_check = 0 self.checker.start(100) - self.search_pool.start_threads() self.pi.startAnimation() def clean_query(self, query): @@ -181,12 +184,12 @@ class SearchDialog(QDialog, Ui_Dialog): if self.hang_check >= HANG_TIME: self.search_pool.abort() self.checker.stop() - self.pi.stopAnimation() + #self.check_progress() else: # Stop the checker if not threads are running. if not self.search_pool.threads_running() and not self.search_pool.has_tasks(): self.checker.stop() - self.pi.stopAnimation() + #self.check_progress() while self.search_pool.has_results(): res, store_plugin = self.search_pool.get_result() @@ -202,6 +205,12 @@ class SearchDialog(QDialog, Ui_Dialog): result = self.results_view.model().get_result(index) self.store_plugins[result.store_name].open(self, result.detail_item) + def check_progress(self): + if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running(): + self.pi.stopAnimation() + else: + self.pi.startAnimation() + def get_store_checks(self): ''' Returns a list of QCheckBox's for each store. From 014d9a51229e77e7a082d75b119e5a9c00a9a2e7 Mon Sep 17 00:00:00 2001 From: John Schember Date: Fri, 22 Apr 2011 20:06:16 -0400 Subject: [PATCH 14/77] Store: Remove unnecessary variables. --- src/calibre/gui2/store/search/search.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 8c3cf8452e..90b8190d66 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -18,8 +18,6 @@ from calibre.gui2.store.search.search_ui import Ui_Dialog HANG_TIME = 75000 # milliseconds seconds TIMEOUT = 75 # seconds -SEARCH_THREAD_TOTAL = 4 -COVER_DOWNLOAD_THREAD_TOTAL = 2 class SearchDialog(QDialog, Ui_Dialog): @@ -31,7 +29,7 @@ class SearchDialog(QDialog, Ui_Dialog): # We keep a cache of store plugins and reference them by name. self.store_plugins = istores - self.search_pool = SearchThreadPool(SEARCH_THREAD_TOTAL) + self.search_pool = SearchThreadPool(4) # Check for results and hung threads. self.checker = QTimer() self.progress_checker = QTimer() @@ -184,12 +182,10 @@ class SearchDialog(QDialog, Ui_Dialog): if self.hang_check >= HANG_TIME: self.search_pool.abort() self.checker.stop() - #self.check_progress() else: # Stop the checker if not threads are running. if not self.search_pool.threads_running() and not self.search_pool.has_tasks(): self.checker.stop() - #self.check_progress() while self.search_pool.has_results(): res, store_plugin = self.search_pool.get_result() From f8e44976e115296320efa693052726f0b94f8e09 Mon Sep 17 00:00:00 2001 From: John Schember Date: Fri, 22 Apr 2011 22:51:36 -0400 Subject: [PATCH 15/77] Store: Advanced search button. --- .../gui2/store/search/adv_search_builder.py | 123 ++++++ .../gui2/store/search/adv_search_builder.ui | 364 ++++++++++++++++++ src/calibre/gui2/store/search/search.py | 11 +- src/calibre/gui2/store/search/search.ui | 7 + 4 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 src/calibre/gui2/store/search/adv_search_builder.py create mode 100644 src/calibre/gui2/store/search/adv_search_builder.ui diff --git a/src/calibre/gui2/store/search/adv_search_builder.py b/src/calibre/gui2/store/search/adv_search_builder.py new file mode 100644 index 0000000000..50d4d3f3f4 --- /dev/null +++ b/src/calibre/gui2/store/search/adv_search_builder.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import re + +from PyQt4.Qt import (QDialog, QDialogButtonBox) + +from calibre.gui2.store.search.adv_search_builder_ui import Ui_Dialog +from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH + +class AdvSearchBuilderDialog(QDialog, Ui_Dialog): + + def __init__(self, parent): + QDialog.__init__(self, parent) + self.setupUi(self) + + self.buttonBox.accepted.connect(self.advanced_search_button_pushed) + self.tab_2_button_box.accepted.connect(self.accept) + self.tab_2_button_box.rejected.connect(self.reject) + self.clear_button.clicked.connect(self.clear_button_pushed) + self.adv_search_used = False + self.mc = '' + + self.tabWidget.setCurrentIndex(0) + self.tabWidget.currentChanged[int].connect(self.tab_changed) + self.tab_changed(0) + + def tab_changed(self, idx): + if idx == 1: + self.tab_2_button_box.button(QDialogButtonBox.Ok).setDefault(True) + else: + self.buttonBox.button(QDialogButtonBox.Ok).setDefault(True) + + def advanced_search_button_pushed(self): + self.adv_search_used = True + self.accept() + + def clear_button_pushed(self): + self.title_box.setText('') + self.author_box.setText('') + self.price_box.setText('') + self.format_box.setText('') + + def tokens(self, raw): + phrases = re.findall(r'\s*".*?"\s*', raw) + for f in phrases: + raw = raw.replace(f, ' ') + phrases = [t.strip('" ') for t in phrases] + return ['"' + self.mc + t + '"' for t in phrases + [r.strip() for r in raw.split()]] + + def search_string(self): + if self.adv_search_used: + return self.adv_search_string() + else: + return self.box_search_string() + + def adv_search_string(self): + mk = self.matchkind.currentIndex() + if mk == CONTAINS_MATCH: + self.mc = '' + elif mk == EQUALS_MATCH: + self.mc = '=' + else: + self.mc = '~' + all, any, phrase, none = map(lambda x: unicode(x.text()), + (self.all, self.any, self.phrase, self.none)) + all, any, none = map(self.tokens, (all, any, none)) + phrase = phrase.strip() + all = ' and '.join(all) + any = ' or '.join(any) + none = ' and not '.join(none) + ans = '' + if phrase: + ans += '"%s"'%phrase + if all: + ans += (' and ' if ans else '') + all + if none: + ans += (' and not ' if ans else 'not ') + none + if any: + ans += (' or ' if ans else '') + any + return ans + + def token(self): + txt = unicode(self.text.text()).strip() + if txt: + if self.negate.isChecked(): + txt = '!'+txt + tok = self.FIELDS[unicode(self.field.currentText())]+txt + if re.search(r'\s', tok): + tok = '"%s"'%tok + return tok + + def box_search_string(self): + mk = self.matchkind.currentIndex() + if mk == CONTAINS_MATCH: + self.mc = '' + elif mk == EQUALS_MATCH: + self.mc = '=' + else: + self.mc = '~' + + ans = [] + self.box_last_values = {} + title = unicode(self.title_box.text()).strip() + if title: + ans.append('title:"' + self.mc + title + '"') + author = unicode(self.author_box.text()).strip() + if author: + ans.append('author:"' + self.mc + author + '"') + price = unicode(self.price_box.text()).strip() + if price: + ans.append('price:"' + self.mc + price + '"') + format = unicode(self.format_box.text()).strip() + if author: + ans.append('format:"' + self.mc + format + '"') + if ans: + return ' and '.join(ans) + return '' diff --git a/src/calibre/gui2/store/search/adv_search_builder.ui b/src/calibre/gui2/store/search/adv_search_builder.ui new file mode 100644 index 0000000000..576f7d3337 --- /dev/null +++ b/src/calibre/gui2/store/search/adv_search_builder.ui @@ -0,0 +1,364 @@ + + + Dialog + + + + 0 + 0 + 752 + 472 + + + + Advanced Search + + + + :/images/search.png:/images/search.png + + + + + + &What kind of match to use: + + + matchkind + + + + + + + + Contains: the word or phrase matches anywhere in the metadata field + + + + + Equals: the word or phrase must match the entire metadata field + + + + + Regular expression: the expression must match anywhere in the metadata field + + + + + + + + 1 + + + + A&dvanced Search + + + + + + Find entries that have... + + + + + + + + &All these words: + + + all + + + + + + + + + + + + + + This exact &phrase: + + + all + + + + + + + + + + + + + + &One or more of these words: + + + all + + + + + + + + + + + + + + + But dont show entries that have... + + + + + + + + Any of these &unwanted words: + + + all + + + + + + + + + + + + + 16777215 + 30 + + + + See the <a href="http://calibre-ebook.com/user_manual/gui.html#the-search-interface">User Manual</a> for more help + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Titl&e/Author/Price ... + + + + + + &Title: + + + title_box + + + + + + + Enter the title. + + + + + + + &Author: + + + author_box + + + + + + + &Price: + + + price_box + + + + + + + + + &Clear + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Search only in specific fields: + + + + + + + + + + + + + &Format: + + + format_box + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + EnLineEdit + QLineEdit +

widgets.h
+ + + + all + phrase + any + none + buttonBox + title_box + author_box + price_box + format_box + clear_button + tab_2_button_box + tabWidget + matchkind + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 90b8190d66..8e9560653a 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -9,10 +9,11 @@ __docformat__ = 'restructuredtext en' import re from random import shuffle -from PyQt4.Qt import (Qt, QDialog, QTimer, QCheckBox, QVBoxLayout) +from PyQt4.Qt import (Qt, QDialog, QTimer, QCheckBox, QVBoxLayout, QIcon) from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator +from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.search.download_thread import SearchThreadPool from calibre.gui2.store.search.search_ui import Ui_Dialog @@ -50,7 +51,10 @@ class SearchDialog(QDialog, Ui_Dialog): # Create and add the progress indicator self.pi = ProgressIndicator(self, 24) self.top_layout.addWidget(self.pi) + + self.adv_search_button.setIcon(QIcon(I('search.png'))) + self.adv_search_button.clicked.connect(self.build_adv_search) self.search.clicked.connect(self.do_search) self.checker.timeout.connect(self.get_results) self.progress_checker.timeout.connect(self.check_progress) @@ -64,6 +68,11 @@ class SearchDialog(QDialog, Ui_Dialog): self.restore_state() + def build_adv_search(self): + adv = AdvSearchBuilderDialog(self) + if adv.exec_() == QDialog.Accepted: + self.search_edit.setText(adv.search_string()) + def resize_columns(self): total = 600 # Cover diff --git a/src/calibre/gui2/store/search/search.ui b/src/calibre/gui2/store/search/search.ui index bdf875113e..0d39a70a29 100644 --- a/src/calibre/gui2/store/search/search.ui +++ b/src/calibre/gui2/store/search/search.ui @@ -30,6 +30,13 @@ + + + + ... + + + From 568b79c018f8ab5829abcd710b5c74e5cce363c8 Mon Sep 17 00:00:00 2001 From: John Schember Date: Fri, 22 Apr 2011 23:02:50 -0400 Subject: [PATCH 16/77] Store: Fix OpenLibrary plugin so it excludes empty books. --- src/calibre/gui2/store/open_library_plugin.py | 5 ++++- src/calibre/gui2/store/search/search.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/store/open_library_plugin.py b/src/calibre/gui2/store/open_library_plugin.py index 93b9c02dcf..b95f1bf930 100644 --- a/src/calibre/gui2/store/open_library_plugin.py +++ b/src/calibre/gui2/store/open_library_plugin.py @@ -50,6 +50,9 @@ class OpenLibraryStore(BasicStoreConfig, StorePlugin): if counter <= 0: break + # Don't include books that don't have downloadable files. + if not data.xpath('boolean(./span[@class="actions"]//span[@class="label" and contains(text(), "Read")])'): + continue id = ''.join(data.xpath('./span[@class="bookcover"]/a/@href')) if not id: continue @@ -67,7 +70,7 @@ class OpenLibraryStore(BasicStoreConfig, StorePlugin): s.author = author.strip() s.price = price s.detail_item = id.strip() - s.drm = SearchResult.DRM_UNKNOWN + s.drm = SearchResult.DRM_UNLOCKED yield s diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 8e9560653a..7e92621932 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -214,7 +214,8 @@ class SearchDialog(QDialog, Ui_Dialog): if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running(): self.pi.stopAnimation() else: - self.pi.startAnimation() + if not self.pi.isAnimated(): + self.pi.startAnimation() def get_store_checks(self): ''' From ae85da399a0320c975ed6b7dce4a9b1cbe741492 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 22 Apr 2011 21:57:35 -0600 Subject: [PATCH 17/77] Fix broken LIT Output. Fixes #769334 (Error converting from [any] to Lit) --- src/calibre/ebooks/oeb/profile.py | 75 --------------------- src/calibre/ebooks/oeb/stylizer.py | 5 +- src/calibre/gui2/store/mobileread_plugin.py | 50 +++++++------- 3 files changed, 27 insertions(+), 103 deletions(-) delete mode 100644 src/calibre/ebooks/oeb/profile.py diff --git a/src/calibre/ebooks/oeb/profile.py b/src/calibre/ebooks/oeb/profile.py deleted file mode 100644 index 17408fac78..0000000000 --- a/src/calibre/ebooks/oeb/profile.py +++ /dev/null @@ -1,75 +0,0 @@ -''' -Device profiles. -''' - -__license__ = 'GPL v3' -__copyright__ = '2008, Marshall T. Vandegrift ' - -from itertools import izip - -FONT_SIZES = [('xx-small', 1), - ('x-small', None), - ('small', 2), - ('medium', 3), - ('large', 4), - ('x-large', 5), - ('xx-large', 6), - (None, 7)] - - -class Profile(object): - def __init__(self, width, height, dpi, fbase, fsizes): - self.width = (float(width) / dpi) * 72. - self.height = (float(height) / dpi) * 72. - self.dpi = float(dpi) - self.fbase = float(fbase) - self.fsizes = [] - for (name, num), size in izip(FONT_SIZES, fsizes): - self.fsizes.append((name, num, float(size))) - self.fnames = dict((name, sz) for name, _, sz in self.fsizes if name) - self.fnums = dict((num, sz) for _, num, sz in self.fsizes if num) - - -PROFILES = { - 'PRS505': - Profile(width=584, height=754, dpi=168.451, fbase=12, - fsizes=[7.5, 9, 10, 12, 15.5, 20, 22, 24]), - - 'MSReader': - Profile(width=480, height=652, dpi=96, fbase=13, - fsizes=[10, 11, 13, 16, 18, 20, 22, 26]), - - # Not really, but let's pretend - 'Mobipocket': - Profile(width=600, height=800, dpi=96, fbase=18, - fsizes=[14, 14, 16, 18, 20, 22, 24, 26]), - - # No clue on usable screen size; DPI should be good - 'HanlinV3': - Profile(width=584, height=754, dpi=168.451, fbase=16, - fsizes=[12, 12, 14, 16, 18, 20, 22, 24]), - - 'CybookG3': - Profile(width=600, height=800, dpi=168.451, fbase=16, - fsizes=[12, 12, 14, 16, 18, 20, 22, 24]), - - 'Kindle': - Profile(width=525, height=640, dpi=168.451, fbase=16, - fsizes=[12, 12, 14, 16, 18, 20, 22, 24]), - - 'Browser': - Profile(width=800, height=600, dpi=100.0, fbase=12, - fsizes=[5, 7, 9, 12, 13.5, 17, 20, 22, 24]) - } - - -class Context(object): - PROFILES = PROFILES - - def __init__(self, source, dest): - if source in PROFILES: - source = PROFILES[source] - if dest in PROFILES: - dest = PROFILES[dest] - self.source = source - self.dest = dest diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 4f06efba9f..b803a7bd68 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -21,7 +21,6 @@ from calibre import force_unicode from calibre.ebooks import unit_convert from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize -from calibre.ebooks.oeb.profile import PROFILES cssutils.log.setLevel(logging.WARN) @@ -123,10 +122,10 @@ class CSSSelector(etree.XPath): class Stylizer(object): STYLESHEETS = WeakKeyDictionary() - def __init__(self, tree, path, oeb, opts, profile=PROFILES['PRS505'], + def __init__(self, tree, path, oeb, opts, profile=None, extra_css='', user_css=''): self.oeb, self.opts = oeb, opts - self.profile = profile + self.profile = opts.input_profile self.logger = oeb.logger item = oeb.manifest.hrefs[path] basename = os.path.basename(path) diff --git a/src/calibre/gui2/store/mobileread_plugin.py b/src/calibre/gui2/store/mobileread_plugin.py index 25125d38c0..86cdb77e4e 100644 --- a/src/calibre/gui2/store/mobileread_plugin.py +++ b/src/calibre/gui2/store/mobileread_plugin.py @@ -27,13 +27,13 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog from calibre.utils.icu import sort_key class MobileReadStore(BasicStoreConfig, StorePlugin): - + def genesis(self): self.rlock = RLock() - + def open(self, parent=None, detail_item=None, external=False): url = 'http://www.mobileread.com/' - + if external or self.config.get('open_external', False): open_url(QUrl(detail_item if detail_item else url)) else: @@ -68,7 +68,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): ratio += s.ratio() if ratio > 0: matches.append((ratio, x)) - + # Move the best scorers to head of list. matches = heapq.nlargest(max_results, matches) for score, book in matches: @@ -79,21 +79,21 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): def update_book_list(self, timeout=10): with self.rlock: url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html' - + last_download = self.config.get('last_download', None) # Don't update the book list if our cache is less than one week old. if last_download and (time.time() - last_download) < 604800: return - + # Download the book list HTML file from MobileRead. br = browser() raw_data = None with closing(br.open(url, timeout=timeout)) as f: raw_data = f.read() - + if not raw_data: return - + # Turn books listed in the HTML file into SearchResults's. books = [] try: @@ -103,7 +103,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): book.detail_item = ''.join(book_data.xpath('.//a/@href')) book.formats = ''.join(book_data.xpath('.//i/text()')) book.formats = book.formats.strip() - + text = ''.join(book_data.xpath('.//a/text()')) if ':' in text: book.author, q, text = text.partition(':') @@ -112,7 +112,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): books.append(book) except: pass - + # Save the book list and it's create time. if books: self.config['last_download'] = time.time() @@ -121,7 +121,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): def get_book_list(self, timeout=10): self.update_book_list(timeout=timeout) return self.deseralize_books(self.config.get('book_list', [])) - + def seralize_books(self, books): sbooks = [] for b in books: @@ -132,7 +132,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): data['formats'] = b.formats sbooks.append(data) return sbooks - + def deseralize_books(self, sbooks): books = [] for s in sbooks: @@ -146,13 +146,13 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): class MobeReadStoreDialog(QDialog, Ui_Dialog): - + def __init__(self, plugin, *args): QDialog.__init__(self, *args) self.setupUi(self) self.plugin = plugin - + self.model = BooksModel() self.results_view.setModel(self.model) self.results_view.model().set_books(self.plugin.get_book_list()) @@ -162,14 +162,14 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog): self.search_query.textChanged.connect(self.model.set_filter) self.results_view.model().total_changed.connect(self.total.setText) self.finished.connect(self.dialog_closed) - + self.restore_state() - + def open_store(self, index): result = self.results_view.model().get_book(index) if result: self.plugin.open(self, result.detail_item) - + def restore_state(self): geometry = self.plugin.config.get('dialog_geometry', None) if geometry: @@ -184,7 +184,7 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog): else: for i in xrange(self.results_view.model().columnCount()): self.results_view.resizeColumnToContents(i) - + self.results_view.model().sort_col = self.plugin.config.get('dialog_sort_col', 0) self.results_view.model().sort_order = self.plugin.config.get('dialog_sort_order', Qt.AscendingOrder) self.results_view.model().sort(self.results_view.model().sort_col, self.results_view.model().sort_order) @@ -201,7 +201,7 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog): class BooksModel(QAbstractItemModel): - + total_changed = pyqtSignal(unicode) HEADERS = [_('Title'), _('Author(s)'), _('Format')] @@ -217,7 +217,7 @@ class BooksModel(QAbstractItemModel): def set_books(self, books): self.books = books self.all_books = books - + self.sort(self.sort_col, self.sort_order) def get_book(self, index): @@ -226,11 +226,11 @@ class BooksModel(QAbstractItemModel): return self.books[row] else: return None - + def set_filter(self, filter): #self.layoutAboutToBeChanged.emit() self.beginResetModel() - + self.filter = unicode(filter) self.books = [] if self.filter: @@ -253,7 +253,7 @@ class BooksModel(QAbstractItemModel): self.endResetModel() #self.layoutChanged.emit() - + def index(self, row, column, parent=QModelIndex()): return self.createIndex(row, column) @@ -267,7 +267,7 @@ class BooksModel(QAbstractItemModel): def columnCount(self, *args): return len(self.HEADERS) - + def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return NONE @@ -307,7 +307,7 @@ class BooksModel(QAbstractItemModel): if not self.books: return - descending = order == Qt.DescendingOrder + descending = order == Qt.DescendingOrder self.books.sort(None, lambda x: sort_key(unicode(self.data_as_text(x, col))), descending) From c8b0bec34066c35604f2d73ba85a22d8930f9660 Mon Sep 17 00:00:00 2001 From: Lee Date: Sat, 23 Apr 2011 14:02:42 +0800 Subject: [PATCH 18/77] fix two situations where the Overdrive plugin returned no results even though the book was in their database --- .../ebooks/metadata/sources/overdrive.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py index e975d41ea6..0b8fc15c1e 100755 --- a/src/calibre/ebooks/metadata/sources/overdrive.py +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -229,28 +229,33 @@ class OverDrive(Source): if int(m.group('displayrecords')) >= 1: results = True elif int(m.group('totalrecords')) >= 1: - xref_q = '' + if int(m.group('totalrecords')) >= 500: + if xref_q.find('+') != -1: + xref_tokens = xref_q.split('+') + xref_q = xref_tokens[0] + else: + xref_q = '' q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q elif int(m.group('totalrecords')) == 0: return '' - return self.sort_ovrdrv_results(raw, title, title_tokens, author, author_tokens) + return self.sort_ovrdrv_results(raw, log, title, title_tokens, author, author_tokens) - def sort_ovrdrv_results(self, raw, title=None, title_tokens=None, author=None, author_tokens=None, ovrdrv_id=None): + def sort_ovrdrv_results(self, raw, log, title=None, title_tokens=None, author=None, author_tokens=None, ovrdrv_id=None): close_matches = [] raw = re.sub('.*?\[\[(?P.*?)\]\].*', '[[\g]]', raw) results = json.loads(raw) - #print results + #log.error('raw results are:'+str(results)) # The search results are either from a keyword search or a multi-format list from a single ID, # sort through the results for closest match/format if results: for reserveid, od_title, subtitle, edition, series, publisher, format, formatid, creators, \ thumbimage, shortdescription, worldcatlink, excerptlink, creatorfile, sorttitle, \ availabletolibrary, availabletoretailer, relevancyrank, unknown1, unknown2, unknown3 in results: - #print "this record's title is "+od_title+", subtitle is "+subtitle+", author[s] are "+creators+", series is "+series + #log.error("this record's title is "+od_title+", subtitle is "+subtitle+", author[s] are "+creators+", series is "+series) if ovrdrv_id is not None and int(formatid) in [1, 50, 410, 900]: - #print "overdrive id is not None, searching based on format type priority" + #log.error('overdrive id is not None, searching based on format type priority') return self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid) else: @@ -282,6 +287,10 @@ class OverDrive(Source): close_matches.insert(0, self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) else: close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) + + elif close_title_match and close_author_match and int(formatid) in [1, 50, 410, 900]: + close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) + if close_matches: return close_matches[0] else: @@ -289,7 +298,7 @@ class OverDrive(Source): else: return '' - def overdrive_get_record(self, br, q, ovrdrv_id): + def overdrive_get_record(self, br, log, q, ovrdrv_id): search_url = q+'SearchResults.aspx?ReserveID={'+ovrdrv_id+'}' results_url = q+'SearchResults.svc/GetResults?sEcho=1&iColumns=18&sColumns=ReserveID%2CTitle%2CSubtitle%2CEdition%2CSeries%2CPublisher%2CFormat%2CFormatID%2CCreators%2CThumbImage%2CShortDescription%2CWorldCatLink%2CExcerptLink%2CCreatorFile%2CSortTitle%2CAvailableToLibrary%2CAvailableToRetailer%2CRelevancyRank&iDisplayStart=0&iDisplayLength=10&sSearch=&bEscapeRegex=true&iSortingCols=1&iSortCol_0=17&sSortDir_0=asc' @@ -311,7 +320,7 @@ class OverDrive(Source): raw = str(list(raw)) clean_cj = mechanize.CookieJar() br.set_cookiejar(clean_cj) - return self.sort_ovrdrv_results(raw, None, None, None, ovrdrv_id) + return self.sort_ovrdrv_results(raw, log, None, None, None, ovrdrv_id) def find_ovrdrv_data(self, br, log, title, author, isbn, ovrdrv_id=None): @@ -319,7 +328,7 @@ class OverDrive(Source): if ovrdrv_id is None: return self.overdrive_search(br, log, q, title, author) else: - return self.overdrive_get_record(br, q, ovrdrv_id) + return self.overdrive_get_record(br, log, q, ovrdrv_id) From 78734955d0882f2064b2c0200783551813f77182 Mon Sep 17 00:00:00 2001 From: Lee Date: Sat, 23 Apr 2011 14:04:52 +0800 Subject: [PATCH 19/77] ... --- src/calibre/ebooks/metadata/sources/overdrive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py index 0b8fc15c1e..94dd61fd9c 100755 --- a/src/calibre/ebooks/metadata/sources/overdrive.py +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -229,7 +229,7 @@ class OverDrive(Source): if int(m.group('displayrecords')) >= 1: results = True elif int(m.group('totalrecords')) >= 1: - if int(m.group('totalrecords')) >= 500: + if int(m.group('totalrecords')) >= 100: if xref_q.find('+') != -1: xref_tokens = xref_q.split('+') xref_q = xref_tokens[0] From e3fa34c0a2b64adc59398fc60619ac51aa294df7 Mon Sep 17 00:00:00 2001 From: Lee Date: Sat, 23 Apr 2011 17:27:53 +0800 Subject: [PATCH 20/77] Overdrive: handle unspecified author with exact title match --- src/calibre/ebooks/metadata/sources/overdrive.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py index 94dd61fd9c..759da45610 100755 --- a/src/calibre/ebooks/metadata/sources/overdrive.py +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -206,6 +206,7 @@ class OverDrive(Source): xref_q = '+'.join(title_tokens) #log.error('Initial query is %s'%initial_q) #log.error('Cross reference query is %s'%xref_q) + q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q query = '{"szKeyword":"'+initial_q+'"}' @@ -233,8 +234,10 @@ class OverDrive(Source): if xref_q.find('+') != -1: xref_tokens = xref_q.split('+') xref_q = xref_tokens[0] + #log.error('xref_q is '+xref_q) else: xref_q = '' + xref_q = '' q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q elif int(m.group('totalrecords')) == 0: return '' @@ -259,9 +262,10 @@ class OverDrive(Source): return self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid) else: - creators = creators.split(', ') + if creators: + creators = creators.split(', ') # if an exact match in a preferred format occurs - if (author and creators[0] == author[0]) and od_title == title and int(formatid) in [1, 50, 410, 900] and thumbimage: + if ((author and creators[0] == author[0]) or (not author and not creators)) and od_title.lower() == title.lower() and int(formatid) in [1, 50, 410, 900] and thumbimage: return self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid) else: From e47e6fa14f70cc3f4fbff688e74718f4155db2d0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 23 Apr 2011 11:15:59 +0100 Subject: [PATCH 21/77] Fix bool searches to not invert the tristate logic. --- src/calibre/library/caches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 92c5ca9b3c..663b2b71ab 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -556,7 +556,7 @@ class ResultCache(SearchQueryParser): # {{{ return matchkind, query def get_bool_matches(self, location, query, candidates): - bools_are_tristate = not self.db_prefs.get('bools_are_tristate') + bools_are_tristate = self.db_prefs.get('bools_are_tristate') loc = self.field_metadata[location]['rec_index'] matches = set() query = icu_lower(query) From b14e8904032da55f9408bc7846faa7c7c2edb8e8 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 23 Apr 2011 13:02:20 +0100 Subject: [PATCH 22/77] Improve error handling when sorting date-subtype composite columns --- src/calibre/library/caches.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 663b2b71ab..16661056e8 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -1005,9 +1005,9 @@ class SortKeyGenerator(object): if sb == 'date': try: val = parse_date(val) - dt = 'datetime' except: - pass + val = UNDEFINED_DATE + dt = 'datetime' elif sb == 'number': try: val = float(val) From 30f22861baa0396fb7260ca2068ff398f4ffc681 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Apr 2011 08:12:31 -0600 Subject: [PATCH 23/77] Fix typo that broke metadata downloading if the user has the txt comments preference turned on --- src/calibre/ebooks/metadata/sources/identify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index bc9070852b..8771274f92 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -412,7 +412,7 @@ def identify(log, abort, # {{{ if msprefs['txt_comments']: for r in results: - if r.plugin.has_html_comments and r.comments: + if r.identify_plugin.has_html_comments and r.comments: r.comments = html2text(r.comments) max_tags = msprefs['max_tags'] From b26e7cf43605b73993e747a7ab055d4cb3ee182c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Apr 2011 09:11:02 -0600 Subject: [PATCH 24/77] Fix #769492 (Key Ctrl+L doesn't work) --- src/calibre/gui2/viewer/documentview.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 06964cda1c..808a764196 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -534,6 +534,7 @@ class DocumentView(QWebView): # {{{ _('&Lookup in dictionary'), self) self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L) self.dictionary_action.triggered.connect(self.lookup) + self.addAction(self.dictionary_action) self.goto_location_action = QAction(_('Go to...'), self) self.goto_location_menu = m = QMenu(self) self.goto_location_actions = a = { From 8b4879e960e3e0c719c86ff8eb49e6b2753d1597 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 23 Apr 2011 16:58:35 +0100 Subject: [PATCH 25/77] Set default last_modified template to "dd MM yyyy" to help avoid confusing parse_date. --- src/calibre/gui2/preferences/create_custom_column.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index fcbaaf181f..433f8fd222 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -158,7 +158,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): { 'isbn': '{identifiers:select(isbn)}', 'formats': '{formats}', - 'last_modified':'''{last_modified:'format_date($, "dd MMM yy")'}''' + 'last_modified':'''{last_modified:'format_date($, "dd MMM yyyy")'}''' }[which]) self.composite_sort_by.setCurrentIndex(2 if which == 'last_modified' else 0) From 687b79988971da7178103339d3ac992506e846c9 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 23 Apr 2011 15:43:32 -0400 Subject: [PATCH 26/77] Store: MobileRead: Boolean and field search. Advanced search builder. --- src/calibre/gui2/store/mobileread_plugin.py | 194 ++++++++++++------ .../gui2/store/mobileread_store_dialog.ui | 19 +- .../gui2/store/search/adv_search_builder.ui | 4 +- 3 files changed, 147 insertions(+), 70 deletions(-) diff --git a/src/calibre/gui2/store/mobileread_plugin.py b/src/calibre/gui2/store/mobileread_plugin.py index 25125d38c0..3547eb555c 100644 --- a/src/calibre/gui2/store/mobileread_plugin.py +++ b/src/calibre/gui2/store/mobileread_plugin.py @@ -10,12 +10,13 @@ import difflib import heapq import time from contextlib import closing +from operator import attrgetter from threading import RLock from lxml import html from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVariant, \ - pyqtSignal + pyqtSignal, QIcon from calibre import browser from calibre.gui2 import open_url, NONE @@ -24,7 +25,11 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog +from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog +from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ + REGEXP_MATCH from calibre.utils.icu import sort_key +from calibre.utils.search_query_parser import SearchQueryParser class MobileReadStore(BasicStoreConfig, StorePlugin): @@ -50,28 +55,10 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): def search(self, query, max_results=10, timeout=60): books = self.get_book_list(timeout=timeout) - query = query.lower() - query_parts = query.split(' ') - matches = [] - s = difflib.SequenceMatcher() - for x in books: - ratio = 0 - t_string = '%s %s' % (x.author.lower(), x.title.lower()) - query_matches = [] - for q in query_parts: - if q in t_string: - query_matches.append(q) - for q in query_matches: - s.set_seq2(q) - for p in t_string.split(' '): - s.set_seq1(p) - ratio += s.ratio() - if ratio > 0: - matches.append((ratio, x)) - - # Move the best scorers to head of list. - matches = heapq.nlargest(max_results, matches) - for score, book in matches: + sf = SearchFilter(books) + matches = sf.parse(query) + + for book in matches: book.price = '$0.00' book.drm = SearchResult.DRM_UNLOCKED yield book @@ -153,23 +140,38 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog): self.plugin = plugin - self.model = BooksModel() - self.results_view.setModel(self.model) - self.results_view.model().set_books(self.plugin.get_book_list()) - self.total.setText('%s' % self.model.rowCount()) + self.adv_search_button.setIcon(QIcon(I('search.png'))) + + self._model = BooksModel(self.plugin.get_book_list()) + self.results_view.setModel(self._model) + self.total.setText('%s' % self.results_view.model().rowCount()) + self.search_button.clicked.connect(self.do_search) + self.adv_search_button.clicked.connect(self.build_adv_search) self.results_view.activated.connect(self.open_store) - self.search_query.textChanged.connect(self.model.set_filter) - self.results_view.model().total_changed.connect(self.total.setText) + self.results_view.model().total_changed.connect(self.update_book_total) self.finished.connect(self.dialog_closed) self.restore_state() + def do_search(self): + self.results_view.model().search(unicode(self.search_query.text())) + def open_store(self, index): result = self.results_view.model().get_book(index) if result: self.plugin.open(self, result.detail_item) + def update_book_total(self, total): + self.total.setText('%s' % total) + + def build_adv_search(self): + adv = AdvSearchBuilderDialog(self) + adv.price_label.hide() + adv.price_box.hide() + if adv.exec_() == QDialog.Accepted: + self.search_query.setText(adv.search_string()) + def restore_state(self): geometry = self.plugin.config.get('dialog_geometry', None) if geometry: @@ -192,7 +194,7 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog): def save_state(self): self.plugin.config['dialog_geometry'] = bytearray(self.saveGeometry()) - self.plugin.config['dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())] + self.plugin.config['dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())] self.plugin.config['dialog_sort_col'] = self.results_view.model().sort_col self.plugin.config['dialog_sort_order'] = self.results_view.model().sort_order @@ -202,24 +204,19 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog): class BooksModel(QAbstractItemModel): - total_changed = pyqtSignal(unicode) + total_changed = pyqtSignal(int) HEADERS = [_('Title'), _('Author(s)'), _('Format')] - def __init__(self): + def __init__(self, all_books): QAbstractItemModel.__init__(self) - self.books = [] - self.all_books = [] + self.books = all_books + self.all_books = all_books self.filter = '' + self.search_filter = SearchFilter(all_books) self.sort_col = 0 self.sort_order = Qt.AscendingOrder - def set_books(self, books): - self.books = books - self.all_books = books - - self.sort(self.sort_col, self.sort_order) - def get_book(self, index): row = index.row() if row < len(self.books): @@ -227,32 +224,17 @@ class BooksModel(QAbstractItemModel): else: return None - def set_filter(self, filter): - #self.layoutAboutToBeChanged.emit() - self.beginResetModel() - - self.filter = unicode(filter) - self.books = [] - if self.filter: - for b in self.all_books: - test = '%s %s %s' % (b.title, b.author, b.formats) - test = test.lower() - include = True - for item in self.filter.split(' '): - item = item.lower() - if item not in test: - include = False - break - if include: - self.books.append(b) - else: + def search(self, filter): + self.filter = filter.strip() + if not self.filter: self.books = self.all_books - - self.sort(self.sort_col, self.sort_order, reset=False) - self.total_changed.emit('%s' % self.rowCount()) - - self.endResetModel() - #self.layoutChanged.emit() + else: + try: + self.books = list(self.search_filter.parse(self.filter)) + except: + self.books = self.all_books + self.sort(self.sort_col, self.sort_order) + self.total_changed.emit(self.rowCount()) def index(self, row, column, parent=QModelIndex()): return self.createIndex(row, column) @@ -304,13 +286,91 @@ class BooksModel(QAbstractItemModel): def sort(self, col, order, reset=True): self.sort_col = col self.sort_order = order - if not self.books: return - descending = order == Qt.DescendingOrder + descending = order == Qt.DescendingOrder self.books.sort(None, lambda x: sort_key(unicode(self.data_as_text(x, col))), descending) if reset: self.reset() + +class SearchFilter(SearchQueryParser): + + USABLE_LOCATIONS = [ + 'all', + 'author', + 'authors', + 'format', + 'formats', + 'title', + ] + + def __init__(self, all_books=[]): + SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS) + self.srs = set(all_books) + + def universal_set(self): + return self.srs + + def get_matches(self, location, query): + location = location.lower().strip() + if location == 'authors': + location = 'author' + elif location == 'formats': + location = 'format' + + matchkind = CONTAINS_MATCH + if len(query) > 1: + if query.startswith('\\'): + query = query[1:] + elif query.startswith('='): + matchkind = EQUALS_MATCH + query = query[1:] + elif query.startswith('~'): + matchkind = REGEXP_MATCH + query = query[1:] + if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D + query = query.lower() + + if location not in self.USABLE_LOCATIONS: + return set([]) + matches = set([]) + all_locs = set(self.USABLE_LOCATIONS) - set(['all']) + locations = all_locs if location == 'all' else [location] + q = { + 'author': lambda x: x.author.lower(), + 'format': attrgetter('formats'), + 'title': lambda x: x.title.lower(), + } + for x in ('author', 'format'): + q[x+'s'] = q[x] + for sr in self.srs: + for locvalue in locations: + accessor = q[locvalue] + if query == 'true': + if accessor(sr) is not None: + matches.add(sr) + continue + if query == 'false': + if accessor(sr) is None: + matches.add(sr) + continue + try: + ### Can't separate authors because comma is used for name sep and author sep + ### Exact match might not get what you want. For that reason, turn author + ### exactmatch searches into contains searches. + if locvalue == 'author' and matchkind == EQUALS_MATCH: + m = CONTAINS_MATCH + else: + m = matchkind + + vals = [accessor(sr)] + if _match(query, vals, m): + matches.add(sr) + break + except ValueError: # Unicode errors + import traceback + traceback.print_exc() + return matches diff --git a/src/calibre/gui2/store/mobileread_store_dialog.ui b/src/calibre/gui2/store/mobileread_store_dialog.ui index 027d5994f0..6d31efab6d 100644 --- a/src/calibre/gui2/store/mobileread_store_dialog.ui +++ b/src/calibre/gui2/store/mobileread_store_dialog.ui @@ -19,13 +19,30 @@ - Search: + &Query: + + + search_query + + + + + + + ... + + + + Search + + + diff --git a/src/calibre/gui2/store/search/adv_search_builder.ui b/src/calibre/gui2/store/search/adv_search_builder.ui index 576f7d3337..a758057311 100644 --- a/src/calibre/gui2/store/search/adv_search_builder.ui +++ b/src/calibre/gui2/store/search/adv_search_builder.ui @@ -50,7 +50,7 @@ - 1 + 0 @@ -217,7 +217,7 @@ - + &Price: From 835107bf21ad0129d5298e630e10d628f9c2e7f7 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 23 Apr 2011 21:22:07 +0100 Subject: [PATCH 27/77] Make naked searches ignore exceptions when using the limit_search configuration option. --- src/calibre/library/caches.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 16661056e8..e4342988b8 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -630,8 +630,11 @@ class ResultCache(SearchQueryParser): # {{{ terms.add(l) if terms: for l in terms: - matches |= self.get_matches(l, query, - candidates=candidates, allow_recursion=allow_recursion) + try: + matches |= self.get_matches(l, query, + candidates=candidates, allow_recursion=allow_recursion) + except: + pass return matches if location in self.field_metadata: From c00cc7e8d348d93aef887999223136a734184f29 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Apr 2011 15:30:35 -0600 Subject: [PATCH 28/77] Ensure the proceed notification dialog is deleted immediately after it is hidden and the callback is called --- src/calibre/gui2/actions/edit_metadata.py | 4 +-- src/calibre/gui2/dialogs/message_box.py | 30 ++++++++++++--------- src/calibre/gui2/metadata/bulk_download2.py | 4 +-- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index bd1e85d8e8..1718123435 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -117,11 +117,11 @@ class EditMetadataAction(InterfaceAction): payload = (id_map, failed_ids, failed_covers) from calibre.gui2.dialogs.message_box import ProceedNotification - p = ProceedNotification(payload, job.html_details, + p = ProceedNotification(self.apply_downloaded_metadata, + payload, job.html_details, _('Download log'), _('Download complete'), msg, det_msg=det_msg, show_copy_button=show_copy_button, parent=self.gui) - p.proceed.connect(self.apply_downloaded_metadata) p.show() def apply_downloaded_metadata(self, payload): diff --git a/src/calibre/gui2/dialogs/message_box.py b/src/calibre/gui2/dialogs/message_box.py index 6034618458..9a51fe4433 100644 --- a/src/calibre/gui2/dialogs/message_box.py +++ b/src/calibre/gui2/dialogs/message_box.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import (QDialog, QIcon, QApplication, QSize, QKeySequence, - QAction, Qt, pyqtSignal, QTextBrowser, QDialogButtonBox, QVBoxLayout) + QAction, Qt, QTextBrowser, QDialogButtonBox, QVBoxLayout) from calibre.constants import __version__ from calibre.gui2.dialogs.message_box_ui import Ui_Dialog @@ -145,15 +145,15 @@ class ViewLog(QDialog): # {{{ class ProceedNotification(MessageBox): # {{{ - proceed = pyqtSignal(object) - - def __init__(self, payload, html_log, log_viewer_title, title, msg, det_msg='', show_copy_button=False, parent=None): + def __init__(self, callback, payload, html_log, log_viewer_title, title, msg, + det_msg='', show_copy_button=False, parent=None): ''' A non modal popup that notifies the user that a background task has - been completed. If they user clicks yes, the proceed signal is emitted - with payload as its argument. + been completed. - :param payload: Arbitrary object, emitted in the proceed signal + :param callback: A callable that is called with payload if the user + asks to proceed. Note that this is always called in the GUI thread + :param payload: Arbitrary object, passed to callback :param html_log: An HTML or plain text log :param log_viewer_title: The title for the log viewer window :param title: The title fo rthis popup @@ -166,25 +166,29 @@ class ProceedNotification(MessageBox): # {{{ self.payload = payload self.html_log = html_log self.log_viewer_title = log_viewer_title - self.finished.connect(self.do_proceed) + self.finished.connect(self.do_proceed, type=Qt.QueuedConnection) self.vlb = self.bb.addButton(_('View log'), self.bb.ActionRole) self.vlb.setIcon(QIcon(I('debug.png'))) self.vlb.clicked.connect(self.show_log) self.det_msg_toggle.setVisible(bool(det_msg)) self.setModal(False) + self.callback = callback def show_log(self): self.log_viewer = ViewLog(self.log_viewer_title, self.html_log, parent=self) def do_proceed(self, result): - if result == self.Accepted: - self.proceed.emit(self.payload) try: - self.proceed.disconnect() - except: - pass + if result == self.Accepted: + self.callback(self.payload) + finally: + # Ensure this notification is garbage collected + self.callback = None + self.setParent(None) + self.finished.disconnect() + self.vlb.clicked.disconnect() # }}} if __name__ == '__main__': diff --git a/src/calibre/gui2/metadata/bulk_download2.py b/src/calibre/gui2/metadata/bulk_download2.py index 2a307fc902..608fa4f7b6 100644 --- a/src/calibre/gui2/metadata/bulk_download2.py +++ b/src/calibre/gui2/metadata/bulk_download2.py @@ -145,10 +145,10 @@ def download(ids, db, do_identify, covers, ans = {} count = 0 all_failed = True - ''' + #''' # Test apply dialog all_failed = do_identify = covers = False - ''' + #''' for i, mi in izip(ids, metadata): if abort.is_set(): log.error('Aborting...') From 85d5bd4bd3eca949b3a9908912a44ecee3af14e2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Apr 2011 15:52:05 -0600 Subject: [PATCH 29/77] ... --- src/calibre/gui2/preferences/behavior.ui | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/preferences/behavior.ui b/src/calibre/gui2/preferences/behavior.ui index 544de1457a..69ebce6acf 100644 --- a/src/calibre/gui2/preferences/behavior.ui +++ b/src/calibre/gui2/preferences/behavior.ui @@ -6,7 +6,7 @@ 0 0 - 672 + 941 563 @@ -22,7 +22,7 @@ 10 - 00 + 0 @@ -50,13 +50,13 @@ - - Yes/No columns have three values (Requires restart) - If checked, Yes/No custom columns values can be Yes, No, or Unknown. If not checked, the values can be Yes or No. + + Yes/No columns have three values (Requires restart) + @@ -304,7 +304,7 @@ If not checked, the values can be Yes or No. - + Reset all disabled &confirmation dialogs From 949fda1a5b395a938abaf24d8032eeada8ae5818 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Apr 2011 18:39:15 -0600 Subject: [PATCH 30/77] ... --- src/calibre/gui2/dialogs/message_box.py | 5 +++++ src/calibre/gui2/metadata/bulk_download2.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/message_box.py b/src/calibre/gui2/dialogs/message_box.py index 9a51fe4433..f9354a0cfc 100644 --- a/src/calibre/gui2/dialogs/message_box.py +++ b/src/calibre/gui2/dialogs/message_box.py @@ -143,6 +143,9 @@ class ViewLog(QDialog): # {{{ QApplication.clipboard().setText(txt) # }}} + +_proceed_memory = [] + class ProceedNotification(MessageBox): # {{{ def __init__(self, callback, payload, html_log, log_viewer_title, title, msg, @@ -174,6 +177,7 @@ class ProceedNotification(MessageBox): # {{{ self.det_msg_toggle.setVisible(bool(det_msg)) self.setModal(False) self.callback = callback + _proceed_memory.append(self) def show_log(self): self.log_viewer = ViewLog(self.log_viewer_title, self.html_log, @@ -189,6 +193,7 @@ class ProceedNotification(MessageBox): # {{{ self.setParent(None) self.finished.disconnect() self.vlb.clicked.disconnect() + _proceed_memory.remove(self) # }}} if __name__ == '__main__': diff --git a/src/calibre/gui2/metadata/bulk_download2.py b/src/calibre/gui2/metadata/bulk_download2.py index 608fa4f7b6..2a307fc902 100644 --- a/src/calibre/gui2/metadata/bulk_download2.py +++ b/src/calibre/gui2/metadata/bulk_download2.py @@ -145,10 +145,10 @@ def download(ids, db, do_identify, covers, ans = {} count = 0 all_failed = True - #''' + ''' # Test apply dialog all_failed = do_identify = covers = False - #''' + ''' for i, mi in izip(ids, metadata): if abort.is_set(): log.error('Aborting...') From 28922fd4e4eb8b5b40bf5039662863c6ba75b1bd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Apr 2011 19:25:32 -0600 Subject: [PATCH 31/77] Fix #769691 (Updated recipe for Honolulu Star Advertiser) --- recipes/staradvertiser.recipe | 47 ++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/recipes/staradvertiser.recipe b/recipes/staradvertiser.recipe index c1ae48fbdc..cce450f1ce 100644 --- a/recipes/staradvertiser.recipe +++ b/recipes/staradvertiser.recipe @@ -1,5 +1,5 @@ __license__ = 'GPL v3' -__copyright__ = '2009-2010, Darko Miletic ' +__copyright__ = '2009-2011, Darko Miletic ' ''' staradvertiser.com ''' @@ -9,7 +9,7 @@ from calibre.web.feeds.news import BasicNewsRecipe class Starbulletin(BasicNewsRecipe): title = 'Honolulu Star Advertiser' __author__ = 'Darko Miletic' - description = "Latest national and local Hawaii sports news" + description = 'Latest national and local Hawaii sports news' publisher = 'Honolulu Star-Advertiser' category = 'news, Honolulu, Hawaii' oldest_article = 2 @@ -19,7 +19,13 @@ class Starbulletin(BasicNewsRecipe): use_embedded_content = False encoding = 'utf8' publication_type = 'newspaper' - extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif} h1,.brown,.postCredit{color: #663300} .storyDeck{font-size: 1.2em; font-weight: bold} ' + masthead_url = 'http://media.staradvertiser.com/designimages/star-advertiser-logo-small.gif' + extra_css = """ + body{font-family: Verdana,Arial,Helvetica,sans-serif} + h1,.brown,.postCredit{color: #663300} + .storyDeck{font-size: 1.2em; font-weight: bold} + img{display: block} + """ conversion_options = { 'comment' : description @@ -28,14 +34,16 @@ class Starbulletin(BasicNewsRecipe): , 'language' : language , 'linearize_tables' : True } - - remove_tags_before = dict(attrs={'id':'storyTitle'}) - remove_tags_after = dict(name='div',attrs={'class':'storytext'}) + keep_only_tags = [ + dict(attrs={'id':'storyTitle'}) + ,dict(attrs={'class':['storyDeck','postCredit']}) + ,dict(name='span',attrs={'class':'brown'}) + ,dict(name='div',attrs={'class':'storytext'}) + ] remove_tags = [ - dict(name=['object','link','script','span']) - ,dict(attrs={'class':'insideStoryImage'}) + dict(name=['object','link','script','span','meta','base','iframe']) + ,dict(attrs={'class':['insideStoryImage','insideStoryAd']}) ,dict(attrs={'name':'fb_share'}) - ,dict(name='div',attrs={'class':'storytext'}) ] feeds = [ @@ -47,3 +55,24 @@ class Starbulletin(BasicNewsRecipe): ,(u'Business' , u'http://www.staradvertiser.com/business/index.rss' ) ,(u'Travel' , u'http://www.staradvertiser.com/travel/index.rss' ) ] + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + for item in soup.findAll('a'): + limg = item.find('img') + if item.string is not None: + str = item.string + item.replaceWith(str) + else: + if limg: + item.name = 'div' + item.attrs = [] + else: + str = self.tag_to_string(item) + item.replaceWith(str) + for item in soup.findAll('img'): + if not item.has_key('alt'): + item['alt'] = 'image' + return soup + \ No newline at end of file From 2b6561b7aac2a7aa6a2a35796e97ab2c1b8fa813 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Apr 2011 19:30:38 -0600 Subject: [PATCH 32/77] Fix #769692 (Updated recipe for Clarin) --- recipes/clarin.recipe | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/recipes/clarin.recipe b/recipes/clarin.recipe index 7bbb663d1d..8793387865 100644 --- a/recipes/clarin.recipe +++ b/recipes/clarin.recipe @@ -1,6 +1,6 @@ __license__ = 'GPL v3' -__copyright__ = '2008-2010, Darko Miletic ' +__copyright__ = '2008-2011, Darko Miletic ' ''' clarin.com ''' @@ -18,11 +18,18 @@ class Clarin(BasicNewsRecipe): use_embedded_content = False no_stylesheets = True encoding = 'utf8' + delay = 1 language = 'es_AR' publication_type = 'newspaper' INDEX = 'http://www.clarin.com' masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg' - extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,serif; font-size: xx-large} .hora{font-weight:bold} .hd p{font-size: small} .nombre-autor{color: #0F325A} ' + extra_css = """ + body{font-family: Arial,Helvetica,sans-serif} + h2{font-family: Georgia,serif; font-size: xx-large} + .hora{font-weight:bold} + .hd p{font-size: small} + .nombre-autor{color: #0F325A} + """ conversion_options = { 'comment' : description @@ -31,7 +38,9 @@ class Clarin(BasicNewsRecipe): , 'language' : language } - keep_only_tags = [dict(attrs={'class':['hd','mt']})] + keep_only_tags = [dict(attrs={'class':['hd','mt']})] + remove_tags = [dict(name=['meta','base','link'])] + remove_attributes = ['lang','_mce_bogus'] feeds = [ (u'Pagina principal', u'http://www.clarin.com/rss/' ) @@ -47,6 +56,10 @@ class Clarin(BasicNewsRecipe): ,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' ) ] + + def get_article_url(self, article): + return article.get('guid', None) + def print_version(self, url): return url + '?print=1' From 82a185744a143c9c45192a4f9087a5b02f9542f6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Apr 2011 20:06:49 -0600 Subject: [PATCH 33/77] Remove border from category panels in the preferences dialog on windows --- src/calibre/gui2/preferences/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py index 9f26bea7ce..e760aa018a 100644 --- a/src/calibre/gui2/preferences/main.py +++ b/src/calibre/gui2/preferences/main.py @@ -87,6 +87,8 @@ class Category(QWidget): # {{{ self.plugins = plugins self.bar = QToolBar(self) + self.bar.setStyleSheet( + 'QToolBar { border: none; background: none }') self.bar.setIconSize(QSize(48, 48)) self.bar.setMovable(False) self.bar.setFloatable(False) From d4de706fc8df27dc6feee8ae6f108a664539bf30 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 23 Apr 2011 23:33:38 -0400 Subject: [PATCH 34/77] Store: Break MobileRead into more manageable pieces. API for updating store caches. --- src/calibre/customize/builtins.py | 2 +- src/calibre/gui2/store/__init__.py | 34 ++ src/calibre/gui2/store/mobileread/__init__.py | 0 .../store/mobileread/cache_progress_dialog.py | 62 +++ .../store/mobileread/cache_progress_dialog.ui | 104 +++++ .../store/mobileread/cache_update_thread.py | 94 +++++ .../store/mobileread/mobileread_plugin.py | 105 +++++ src/calibre/gui2/store/mobileread/models.py | 190 +++++++++ .../gui2/store/mobileread/store_dialog.py | 83 ++++ .../store_dialog.ui} | 0 src/calibre/gui2/store/mobileread_plugin.py | 376 ------------------ .../gui2/store/search/download_thread.py | 30 ++ src/calibre/gui2/store/search/search.py | 22 +- 13 files changed, 716 insertions(+), 386 deletions(-) create mode 100644 src/calibre/gui2/store/mobileread/__init__.py create mode 100644 src/calibre/gui2/store/mobileread/cache_progress_dialog.py create mode 100644 src/calibre/gui2/store/mobileread/cache_progress_dialog.ui create mode 100644 src/calibre/gui2/store/mobileread/cache_update_thread.py create mode 100644 src/calibre/gui2/store/mobileread/mobileread_plugin.py create mode 100644 src/calibre/gui2/store/mobileread/models.py create mode 100644 src/calibre/gui2/store/mobileread/store_dialog.py rename src/calibre/gui2/store/{mobileread_store_dialog.ui => mobileread/store_dialog.ui} (100%) delete mode 100644 src/calibre/gui2/store/mobileread_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 00af4e5117..c27fa2a57b 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1162,7 +1162,7 @@ class StoreManyBooksStore(StoreBase): class StoreMobileReadStore(StoreBase): name = 'MobileRead' description = _('Ebooks handcrafted with the utmost care') - actual_plugin = 'calibre.gui2.store.mobileread_plugin:MobileReadStore' + actual_plugin = 'calibre.gui2.store.mobileread.mobileread_plugin:MobileReadStore' class StoreOpenLibraryStore(StoreBase): name = 'Open Library' diff --git a/src/calibre/gui2/store/__init__.py b/src/calibre/gui2/store/__init__.py index 16e9f5689d..c95d794975 100644 --- a/src/calibre/gui2/store/__init__.py +++ b/src/calibre/gui2/store/__init__.py @@ -127,6 +127,40 @@ class StorePlugin(object): # {{{ ''' return False + def update_cache(self, parent=None, timeout=60, force=False, suppress_progress=False): + ''' + Some plugins need to keep an local cache of available books. This function + is called to update the caches. It is recommended to call this function + from :meth:`open`. Especially if :meth:`open` does anything other than + open a web page. + + This function can be called at any time. It is up to the plugin to determine + if the cache really does need updating. Unless :param:`force` is True, then + the plugin must update the cache. The only time force should be True is if + this function is called by the plugin's configuration dialog. + + if :param:`suppress_progress` is False it is safe to assume that this function + is being called from the main GUI thread so it is safe and recommended to use + a QProgressDialog to display what is happening and allow the user to cancel + the operation. if :param:`suppress_progress` is True then run the update + silently. In this case there is no guarantee what thread is calling this + function so no Qt related functionality that requires being run in the main + GUI thread should be run. E.G. Open a QProgressDialog. + + :param parent: The parent object to be used by an GUI dialogs. + + :param timeout: The maximum amount of time that should be spent in + any given network connection. + + :param force: Force updating the cache even if the plugin has determined + it is not necessary. + + :param suppress_progress: Should a progress indicator be shown. + + :return: True if the cache was updated, False otherwise. + ''' + return False + def get_settings(self): ''' This is only useful for plugins that implement diff --git a/src/calibre/gui2/store/mobileread/__init__.py b/src/calibre/gui2/store/mobileread/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/gui2/store/mobileread/cache_progress_dialog.py b/src/calibre/gui2/store/mobileread/cache_progress_dialog.py new file mode 100644 index 0000000000..3c372144c4 --- /dev/null +++ b/src/calibre/gui2/store/mobileread/cache_progress_dialog.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import (QCoreApplication, QDialog, QTimer) + +from calibre.gui2.store.mobileread.cache_progress_dialog_ui import Ui_Dialog + +class CacheProgressDialog(QDialog, Ui_Dialog): + + def __init__(self, parent=None, total=None): + QDialog.__init__(self, parent) + self.setupUi(self) + + self.completed = 0 + self.canceled = False + + self.progress.setValue(0) + self.progress.setMinimum(0) + self.progress.setMaximum(total if total else 0) + + def exec_(self): + self.completed = 0 + self.canceled = False + QDialog.exec_(self) + + def open(self): + self.completed = 0 + self.canceled = False + QDialog.open(self) + + def reject(self): + self.canceled = True + QDialog.reject(self) + + def update_progress(self): + ''' + completed is an int from 0 to total representing the number + records that have bee completed. + ''' + self.set_progress(self.completed + 1) + + def set_message(self, msg): + self.message.setText(msg) + + def set_details(self, msg): + self.details.setText(msg) + + def set_progress(self, completed): + ''' + completed is an int from 0 to total representing the number + records that have bee completed. + ''' + self.completed = completed + self.progress.setValue(self.completed) + + def set_total(self, total): + self.progress.setMaximum(total) diff --git a/src/calibre/gui2/store/mobileread/cache_progress_dialog.ui b/src/calibre/gui2/store/mobileread/cache_progress_dialog.ui new file mode 100644 index 0000000000..4690f14e7f --- /dev/null +++ b/src/calibre/gui2/store/mobileread/cache_progress_dialog.ui @@ -0,0 +1,104 @@ + + + Dialog + + + + 0 + 0 + 402 + 138 + + + + Dialog + + + + + + Updating book cache + + + Qt::AlignCenter + + + + + + + 24 + + + + + + + + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/calibre/gui2/store/mobileread/cache_update_thread.py b/src/calibre/gui2/store/mobileread/cache_update_thread.py new file mode 100644 index 0000000000..f81e7951d4 --- /dev/null +++ b/src/calibre/gui2/store/mobileread/cache_update_thread.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import time +from contextlib import closing +from threading import Thread + +from lxml import html + +from PyQt4.Qt import (pyqtSignal, QObject) + +from calibre import browser +from calibre.gui2.store.search_result import SearchResult + +class CacheUpdateThread(Thread, QObject): + + total_changed = pyqtSignal(int) + update_progress = pyqtSignal(int) + update_details = pyqtSignal(unicode) + + def __init__(self, config, seralize_books_function, timeout): + Thread.__init__(self) + QObject.__init__(self) + + self.daemon = True + self.config = config + self.seralize_books = seralize_books_function + self.timeout = timeout + self._run = True + + def abort(self): + self._run = False + + def run(self): + url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html' + + self.update_details.emit(_('Checking last download date.')) + last_download = self.config.get('last_download', None) + # Don't update the book list if our cache is less than one week old. + if last_download and (time.time() - last_download) < 604800: + return + + self.update_details.emit(_('Downloading book list from MobileRead.')) + # Download the book list HTML file from MobileRead. + br = browser() + raw_data = None + try: + with closing(br.open(url, timeout=self.timeout)) as f: + raw_data = f.read() + except: + return + + if not raw_data or not self._run: + return + + self.update_details.emit(_('Processing books.')) + # Turn books listed in the HTML file into SearchResults's. + books = [] + try: + data = html.fromstring(raw_data) + raw_books = data.xpath('//ul/li') + self.total_changed.emit(len(raw_books)) + + for i, book_data in enumerate(raw_books): + self.update_details.emit(_('%s of %s books processed.') % (i, len(raw_books))) + book = SearchResult() + book.detail_item = ''.join(book_data.xpath('.//a/@href')) + book.formats = ''.join(book_data.xpath('.//i/text()')) + book.formats = book.formats.strip() + + text = ''.join(book_data.xpath('.//a/text()')) + if ':' in text: + book.author, q, text = text.partition(':') + book.author = book.author.strip() + book.title = text.strip() + books.append(book) + + if not self._run: + books = [] + break + else: + self.update_progress.emit(i) + except: + pass + + # Save the book list and it's create time. + if books: + self.config['book_list'] = self.seralize_books(books) + self.config['last_download'] = time.time() diff --git a/src/calibre/gui2/store/mobileread/mobileread_plugin.py b/src/calibre/gui2/store/mobileread/mobileread_plugin.py new file mode 100644 index 0000000000..54242ce0b2 --- /dev/null +++ b/src/calibre/gui2/store/mobileread/mobileread_plugin.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from threading import Lock + +from PyQt4.Qt import (QUrl, QCoreApplication) + +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog +from calibre.gui2.store.mobileread.models import SearchFilter +from calibre.gui2.store.mobileread.cache_progress_dialog import CacheProgressDialog +from calibre.gui2.store.mobileread.cache_update_thread import CacheUpdateThread +from calibre.gui2.store.mobileread.store_dialog import MobeReadStoreDialog + +class MobileReadStore(BasicStoreConfig, StorePlugin): + + def genesis(self): + self.lock = Lock() + + def open(self, parent=None, detail_item=None, external=False): + url = 'http://www.mobileread.com/' + + if external or self.config.get('open_external', False): + open_url(QUrl(detail_item if detail_item else url)) + else: + if detail_item: + d = WebStoreDialog(self.gui, url, parent, detail_item) + d.setWindowTitle(self.name) + d.set_tags(self.config.get('tags', '')) + d.exec_() + else: + if self.update_cache(parent, 30): + d = MobeReadStoreDialog(self, parent) + d.setWindowTitle(self.name) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + books = self.get_book_list() + + sf = SearchFilter(books) + matches = sf.parse(query) + + for book in matches: + book.price = '$0.00' + book.drm = SearchResult.DRM_UNLOCKED + yield book + + def update_cache(self, parent=None, timeout=10, force=False, suppress_progress=False): + if self.lock.acquire(False): + try: + update_thread = CacheUpdateThread(self.config, self.seralize_books, timeout) + if not suppress_progress: + progress = CacheProgressDialog(parent) + progress.set_message(_('Updating MobileRead book cache...')) + + update_thread.total_changed.connect(progress.set_total) + update_thread.update_progress.connect(progress.set_progress) + update_thread.update_details.connect(progress.set_details) + progress.rejected.connect(update_thread.abort) + + progress.open() + update_thread.start() + while update_thread.is_alive() and not progress.canceled: + QCoreApplication.processEvents() + + if progress.isVisible(): + progress.accept() + return not progress.canceled + else: + update_thread.start() + finally: + self.lock.release() + + def get_book_list(self): + return self.deseralize_books(self.config.get('book_list', [])) + + def seralize_books(self, books): + sbooks = [] + for b in books: + data = {} + data['author'] = b.author + data['title'] = b.title + data['detail_item'] = b.detail_item + data['formats'] = b.formats + sbooks.append(data) + return sbooks + + def deseralize_books(self, sbooks): + books = [] + for s in sbooks: + b = SearchResult() + b.author = s.get('author', '') + b.title = s.get('title', '') + b.detail_item = s.get('detail_item', '') + b.formats = s.get('formats', '') + books.append(b) + return books diff --git a/src/calibre/gui2/store/mobileread/models.py b/src/calibre/gui2/store/mobileread/models.py new file mode 100644 index 0000000000..a080affb51 --- /dev/null +++ b/src/calibre/gui2/store/mobileread/models.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from operator import attrgetter + +from PyQt4.Qt import (Qt, QAbstractItemModel, QModelIndex, QVariant, pyqtSignal) + +from calibre.gui2 import NONE +from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ + REGEXP_MATCH +from calibre.utils.icu import sort_key +from calibre.utils.search_query_parser import SearchQueryParser + +class BooksModel(QAbstractItemModel): + + total_changed = pyqtSignal(int) + + HEADERS = [_('Title'), _('Author(s)'), _('Format')] + + def __init__(self, all_books): + QAbstractItemModel.__init__(self) + self.books = all_books + self.all_books = all_books + self.filter = '' + self.search_filter = SearchFilter(all_books) + self.sort_col = 0 + self.sort_order = Qt.AscendingOrder + + def get_book(self, index): + row = index.row() + if row < len(self.books): + return self.books[row] + else: + return None + + def search(self, filter): + self.filter = filter.strip() + if not self.filter: + self.books = self.all_books + else: + try: + self.books = list(self.search_filter.parse(self.filter)) + except: + self.books = self.all_books + self.sort(self.sort_col, self.sort_order) + self.total_changed.emit(self.rowCount()) + + def index(self, row, column, parent=QModelIndex()): + return self.createIndex(row, column) + + def parent(self, index): + if not index.isValid() or index.internalId() == 0: + return QModelIndex() + return self.createIndex(0, 0) + + def rowCount(self, *args): + return len(self.books) + + def columnCount(self, *args): + return len(self.HEADERS) + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return NONE + text = '' + if orientation == Qt.Horizontal: + if section < len(self.HEADERS): + text = self.HEADERS[section] + return QVariant(text) + else: + return QVariant(section+1) + + def data(self, index, role): + row, col = index.row(), index.column() + result = self.books[row] + if role == Qt.DisplayRole: + if col == 0: + return QVariant(result.title) + elif col == 1: + return QVariant(result.author) + elif col == 2: + return QVariant(result.formats) + return NONE + + def data_as_text(self, result, col): + text = '' + if col == 0: + text = result.title + elif col == 1: + text = result.author + elif col == 2: + text = result.formats + return text + + def sort(self, col, order, reset=True): + self.sort_col = col + self.sort_order = order + if not self.books: + return + descending = order == Qt.DescendingOrder + self.books.sort(None, + lambda x: sort_key(unicode(self.data_as_text(x, col))), + descending) + if reset: + self.reset() + + +class SearchFilter(SearchQueryParser): + + USABLE_LOCATIONS = [ + 'all', + 'author', + 'authors', + 'format', + 'formats', + 'title', + ] + + def __init__(self, all_books=[]): + SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS) + self.srs = set(all_books) + + def universal_set(self): + return self.srs + + def get_matches(self, location, query): + location = location.lower().strip() + if location == 'authors': + location = 'author' + elif location == 'formats': + location = 'format' + + matchkind = CONTAINS_MATCH + if len(query) > 1: + if query.startswith('\\'): + query = query[1:] + elif query.startswith('='): + matchkind = EQUALS_MATCH + query = query[1:] + elif query.startswith('~'): + matchkind = REGEXP_MATCH + query = query[1:] + if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D + query = query.lower() + + if location not in self.USABLE_LOCATIONS: + return set([]) + matches = set([]) + all_locs = set(self.USABLE_LOCATIONS) - set(['all']) + locations = all_locs if location == 'all' else [location] + q = { + 'author': lambda x: x.author.lower(), + 'format': attrgetter('formats'), + 'title': lambda x: x.title.lower(), + } + for x in ('author', 'format'): + q[x+'s'] = q[x] + for sr in self.srs: + for locvalue in locations: + accessor = q[locvalue] + if query == 'true': + if accessor(sr) is not None: + matches.add(sr) + continue + if query == 'false': + if accessor(sr) is None: + matches.add(sr) + continue + try: + ### Can't separate authors because comma is used for name sep and author sep + ### Exact match might not get what you want. For that reason, turn author + ### exactmatch searches into contains searches. + if locvalue == 'author' and matchkind == EQUALS_MATCH: + m = CONTAINS_MATCH + else: + m = matchkind + + vals = [accessor(sr)] + if _match(query, vals, m): + matches.add(sr) + break + except ValueError: # Unicode errors + import traceback + traceback.print_exc() + return matches diff --git a/src/calibre/gui2/store/mobileread/store_dialog.py b/src/calibre/gui2/store/mobileread/store_dialog.py new file mode 100644 index 0000000000..af300565aa --- /dev/null +++ b/src/calibre/gui2/store/mobileread/store_dialog.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + + +from PyQt4.Qt import (Qt, QDialog, QIcon) + +from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog +from calibre.gui2.store.mobileread.models import BooksModel +from calibre.gui2.store.mobileread.store_dialog_ui import Ui_Dialog + +class MobeReadStoreDialog(QDialog, Ui_Dialog): + + def __init__(self, plugin, *args): + QDialog.__init__(self, *args) + self.setupUi(self) + + self.plugin = plugin + + self.adv_search_button.setIcon(QIcon(I('search.png'))) + + self._model = BooksModel(self.plugin.get_book_list()) + self.results_view.setModel(self._model) + self.total.setText('%s' % self.results_view.model().rowCount()) + + self.search_button.clicked.connect(self.do_search) + self.adv_search_button.clicked.connect(self.build_adv_search) + self.results_view.activated.connect(self.open_store) + self.results_view.model().total_changed.connect(self.update_book_total) + self.finished.connect(self.dialog_closed) + + self.restore_state() + + def do_search(self): + self.results_view.model().search(unicode(self.search_query.text())) + + def open_store(self, index): + result = self.results_view.model().get_book(index) + if result: + self.plugin.open(self, result.detail_item) + + def update_book_total(self, total): + self.total.setText('%s' % total) + + def build_adv_search(self): + adv = AdvSearchBuilderDialog(self) + adv.price_label.hide() + adv.price_box.hide() + if adv.exec_() == QDialog.Accepted: + self.search_query.setText(adv.search_string()) + + def restore_state(self): + geometry = self.plugin.config.get('dialog_geometry', None) + if geometry: + self.restoreGeometry(geometry) + + results_cwidth = self.plugin.config.get('dialog_results_view_column_width') + if results_cwidth: + for i, x in enumerate(results_cwidth): + if i >= self.results_view.model().columnCount(): + break + self.results_view.setColumnWidth(i, x) + else: + for i in xrange(self.results_view.model().columnCount()): + self.results_view.resizeColumnToContents(i) + + self.results_view.model().sort_col = self.plugin.config.get('dialog_sort_col', 0) + self.results_view.model().sort_order = self.plugin.config.get('dialog_sort_order', Qt.AscendingOrder) + self.results_view.model().sort(self.results_view.model().sort_col, self.results_view.model().sort_order) + self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order) + + def save_state(self): + self.plugin.config['dialog_geometry'] = bytearray(self.saveGeometry()) + self.plugin.config['dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())] + self.plugin.config['dialog_sort_col'] = self.results_view.model().sort_col + self.plugin.config['dialog_sort_order'] = self.results_view.model().sort_order + + def dialog_closed(self, result): + self.save_state() diff --git a/src/calibre/gui2/store/mobileread_store_dialog.ui b/src/calibre/gui2/store/mobileread/store_dialog.ui similarity index 100% rename from src/calibre/gui2/store/mobileread_store_dialog.ui rename to src/calibre/gui2/store/mobileread/store_dialog.ui diff --git a/src/calibre/gui2/store/mobileread_plugin.py b/src/calibre/gui2/store/mobileread_plugin.py deleted file mode 100644 index 3547eb555c..0000000000 --- a/src/calibre/gui2/store/mobileread_plugin.py +++ /dev/null @@ -1,376 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import (unicode_literals, division, absolute_import, print_function) - -__license__ = 'GPL 3' -__copyright__ = '2011, John Schember ' -__docformat__ = 'restructuredtext en' - -import difflib -import heapq -import time -from contextlib import closing -from operator import attrgetter -from threading import RLock - -from lxml import html - -from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVariant, \ - pyqtSignal, QIcon - -from calibre import browser -from calibre.gui2 import open_url, NONE -from calibre.gui2.store import StorePlugin -from calibre.gui2.store.basic_config import BasicStoreConfig -from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog -from calibre.gui2.store.search_result import SearchResult -from calibre.gui2.store.web_store_dialog import WebStoreDialog -from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog -from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ - REGEXP_MATCH -from calibre.utils.icu import sort_key -from calibre.utils.search_query_parser import SearchQueryParser - -class MobileReadStore(BasicStoreConfig, StorePlugin): - - def genesis(self): - self.rlock = RLock() - - def open(self, parent=None, detail_item=None, external=False): - url = 'http://www.mobileread.com/' - - if external or self.config.get('open_external', False): - open_url(QUrl(detail_item if detail_item else url)) - else: - if detail_item: - d = WebStoreDialog(self.gui, url, parent, detail_item) - d.setWindowTitle(self.name) - d.set_tags(self.config.get('tags', '')) - d.exec_() - else: - d = MobeReadStoreDialog(self, parent) - d.setWindowTitle(self.name) - d.exec_() - - def search(self, query, max_results=10, timeout=60): - books = self.get_book_list(timeout=timeout) - - sf = SearchFilter(books) - matches = sf.parse(query) - - for book in matches: - book.price = '$0.00' - book.drm = SearchResult.DRM_UNLOCKED - yield book - - def update_book_list(self, timeout=10): - with self.rlock: - url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html' - - last_download = self.config.get('last_download', None) - # Don't update the book list if our cache is less than one week old. - if last_download and (time.time() - last_download) < 604800: - return - - # Download the book list HTML file from MobileRead. - br = browser() - raw_data = None - with closing(br.open(url, timeout=timeout)) as f: - raw_data = f.read() - - if not raw_data: - return - - # Turn books listed in the HTML file into SearchResults's. - books = [] - try: - data = html.fromstring(raw_data) - for book_data in data.xpath('//ul/li'): - book = SearchResult() - book.detail_item = ''.join(book_data.xpath('.//a/@href')) - book.formats = ''.join(book_data.xpath('.//i/text()')) - book.formats = book.formats.strip() - - text = ''.join(book_data.xpath('.//a/text()')) - if ':' in text: - book.author, q, text = text.partition(':') - book.author = book.author.strip() - book.title = text.strip() - books.append(book) - except: - pass - - # Save the book list and it's create time. - if books: - self.config['last_download'] = time.time() - self.config['book_list'] = self.seralize_books(books) - - def get_book_list(self, timeout=10): - self.update_book_list(timeout=timeout) - return self.deseralize_books(self.config.get('book_list', [])) - - def seralize_books(self, books): - sbooks = [] - for b in books: - data = {} - data['author'] = b.author - data['title'] = b.title - data['detail_item'] = b.detail_item - data['formats'] = b.formats - sbooks.append(data) - return sbooks - - def deseralize_books(self, sbooks): - books = [] - for s in sbooks: - b = SearchResult() - b.author = s.get('author', '') - b.title = s.get('title', '') - b.detail_item = s.get('detail_item', '') - b.formats = s.get('formats', '') - books.append(b) - return books - - -class MobeReadStoreDialog(QDialog, Ui_Dialog): - - def __init__(self, plugin, *args): - QDialog.__init__(self, *args) - self.setupUi(self) - - self.plugin = plugin - - self.adv_search_button.setIcon(QIcon(I('search.png'))) - - self._model = BooksModel(self.plugin.get_book_list()) - self.results_view.setModel(self._model) - self.total.setText('%s' % self.results_view.model().rowCount()) - - self.search_button.clicked.connect(self.do_search) - self.adv_search_button.clicked.connect(self.build_adv_search) - self.results_view.activated.connect(self.open_store) - self.results_view.model().total_changed.connect(self.update_book_total) - self.finished.connect(self.dialog_closed) - - self.restore_state() - - def do_search(self): - self.results_view.model().search(unicode(self.search_query.text())) - - def open_store(self, index): - result = self.results_view.model().get_book(index) - if result: - self.plugin.open(self, result.detail_item) - - def update_book_total(self, total): - self.total.setText('%s' % total) - - def build_adv_search(self): - adv = AdvSearchBuilderDialog(self) - adv.price_label.hide() - adv.price_box.hide() - if adv.exec_() == QDialog.Accepted: - self.search_query.setText(adv.search_string()) - - def restore_state(self): - geometry = self.plugin.config.get('dialog_geometry', None) - if geometry: - self.restoreGeometry(geometry) - - results_cwidth = self.plugin.config.get('dialog_results_view_column_width') - if results_cwidth: - for i, x in enumerate(results_cwidth): - if i >= self.results_view.model().columnCount(): - break - self.results_view.setColumnWidth(i, x) - else: - for i in xrange(self.results_view.model().columnCount()): - self.results_view.resizeColumnToContents(i) - - self.results_view.model().sort_col = self.plugin.config.get('dialog_sort_col', 0) - self.results_view.model().sort_order = self.plugin.config.get('dialog_sort_order', Qt.AscendingOrder) - self.results_view.model().sort(self.results_view.model().sort_col, self.results_view.model().sort_order) - self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order) - - def save_state(self): - self.plugin.config['dialog_geometry'] = bytearray(self.saveGeometry()) - self.plugin.config['dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())] - self.plugin.config['dialog_sort_col'] = self.results_view.model().sort_col - self.plugin.config['dialog_sort_order'] = self.results_view.model().sort_order - - def dialog_closed(self, result): - self.save_state() - - -class BooksModel(QAbstractItemModel): - - total_changed = pyqtSignal(int) - - HEADERS = [_('Title'), _('Author(s)'), _('Format')] - - def __init__(self, all_books): - QAbstractItemModel.__init__(self) - self.books = all_books - self.all_books = all_books - self.filter = '' - self.search_filter = SearchFilter(all_books) - self.sort_col = 0 - self.sort_order = Qt.AscendingOrder - - def get_book(self, index): - row = index.row() - if row < len(self.books): - return self.books[row] - else: - return None - - def search(self, filter): - self.filter = filter.strip() - if not self.filter: - self.books = self.all_books - else: - try: - self.books = list(self.search_filter.parse(self.filter)) - except: - self.books = self.all_books - self.sort(self.sort_col, self.sort_order) - self.total_changed.emit(self.rowCount()) - - def index(self, row, column, parent=QModelIndex()): - return self.createIndex(row, column) - - def parent(self, index): - if not index.isValid() or index.internalId() == 0: - return QModelIndex() - return self.createIndex(0, 0) - - def rowCount(self, *args): - return len(self.books) - - def columnCount(self, *args): - return len(self.HEADERS) - - def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: - return NONE - text = '' - if orientation == Qt.Horizontal: - if section < len(self.HEADERS): - text = self.HEADERS[section] - return QVariant(text) - else: - return QVariant(section+1) - - def data(self, index, role): - row, col = index.row(), index.column() - result = self.books[row] - if role == Qt.DisplayRole: - if col == 0: - return QVariant(result.title) - elif col == 1: - return QVariant(result.author) - elif col == 2: - return QVariant(result.formats) - return NONE - - def data_as_text(self, result, col): - text = '' - if col == 0: - text = result.title - elif col == 1: - text = result.author - elif col == 2: - text = result.formats - return text - - def sort(self, col, order, reset=True): - self.sort_col = col - self.sort_order = order - if not self.books: - return - descending = order == Qt.DescendingOrder - self.books.sort(None, - lambda x: sort_key(unicode(self.data_as_text(x, col))), - descending) - if reset: - self.reset() - - -class SearchFilter(SearchQueryParser): - - USABLE_LOCATIONS = [ - 'all', - 'author', - 'authors', - 'format', - 'formats', - 'title', - ] - - def __init__(self, all_books=[]): - SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS) - self.srs = set(all_books) - - def universal_set(self): - return self.srs - - def get_matches(self, location, query): - location = location.lower().strip() - if location == 'authors': - location = 'author' - elif location == 'formats': - location = 'format' - - matchkind = CONTAINS_MATCH - if len(query) > 1: - if query.startswith('\\'): - query = query[1:] - elif query.startswith('='): - matchkind = EQUALS_MATCH - query = query[1:] - elif query.startswith('~'): - matchkind = REGEXP_MATCH - query = query[1:] - if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D - query = query.lower() - - if location not in self.USABLE_LOCATIONS: - return set([]) - matches = set([]) - all_locs = set(self.USABLE_LOCATIONS) - set(['all']) - locations = all_locs if location == 'all' else [location] - q = { - 'author': lambda x: x.author.lower(), - 'format': attrgetter('formats'), - 'title': lambda x: x.title.lower(), - } - for x in ('author', 'format'): - q[x+'s'] = q[x] - for sr in self.srs: - for locvalue in locations: - accessor = q[locvalue] - if query == 'true': - if accessor(sr) is not None: - matches.add(sr) - continue - if query == 'false': - if accessor(sr) is None: - matches.add(sr) - continue - try: - ### Can't separate authors because comma is used for name sep and author sep - ### Exact match might not get what you want. For that reason, turn author - ### exactmatch searches into contains searches. - if locvalue == 'author' and matchkind == EQUALS_MATCH: - m = CONTAINS_MATCH - else: - m = matchkind - - vals = [accessor(sr)] - if _match(query, vals, m): - matches.add(sr) - break - except ValueError: # Unicode errors - import traceback - traceback.print_exc() - return matches diff --git a/src/calibre/gui2/store/search/download_thread.py b/src/calibre/gui2/store/search/download_thread.py index 4dd3c4a59b..6dd59cc5a7 100644 --- a/src/calibre/gui2/store/search/download_thread.py +++ b/src/calibre/gui2/store/search/download_thread.py @@ -192,3 +192,33 @@ class DetailsThread(Thread): self.tasks.task_done() except: continue + + +class CacheUpdateThreadPool(GenericDownloadThreadPool): + + def __init__(self, thread_count): + GenericDownloadThreadPool.__init__(self, CacheUpdateThread, thread_count) + + def add_task(self, store_plugin, timeout=10): + self.tasks.put((store_plugin, timeout)) + GenericDownloadThreadPool.add_task(self) + + +class CacheUpdateThread(Thread): + + def __init__(self, tasks, results): + Thread.__init__(self) + self.daemon = True + self.tasks = tasks + self._run = True + + def abort(self): + self._run = False + + def run(self): + while self._run and not self.tasks.empty(): + try: + store_plugin, timeout = self.tasks.get() + store_plugin.update_cache(timeout=timeout, suppress_progress=True) + except: + traceback.print_exc() diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 7e92621932..70e92d1756 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -14,7 +14,8 @@ from PyQt4.Qt import (Qt, QDialog, QTimer, QCheckBox, QVBoxLayout, QIcon) from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog -from calibre.gui2.store.search.download_thread import SearchThreadPool +from calibre.gui2.store.search.download_thread import SearchThreadPool, \ + CacheUpdateThreadPool from calibre.gui2.store.search.search_ui import Ui_Dialog HANG_TIME = 75000 # milliseconds seconds @@ -31,10 +32,15 @@ class SearchDialog(QDialog, Ui_Dialog): # We keep a cache of store plugins and reference them by name. self.store_plugins = istores self.search_pool = SearchThreadPool(4) + self.cache_pool = CacheUpdateThreadPool(2) # Check for results and hung threads. self.checker = QTimer() self.progress_checker = QTimer() self.hang_check = 0 + + # Update store caches silently. + for p in self.store_plugins.values(): + self.cache_pool.add_task(p, 30) # Add check boxes for each store so the user # can disable searching specific stores on a @@ -116,10 +122,9 @@ class SearchDialog(QDialog, Ui_Dialog): for n in store_names: if getattr(self, 'store_check_' + n).isChecked(): self.search_pool.add_task(query, n, self.store_plugins[n], TIMEOUT) - if self.search_pool.has_tasks() or self.search_pool.threads_running(): - self.hang_check = 0 - self.checker.start(100) - self.pi.startAnimation() + self.hang_check = 0 + self.checker.start(100) + self.pi.startAnimation() def clean_query(self, query): query = query.lower() @@ -200,10 +205,9 @@ class SearchDialog(QDialog, Ui_Dialog): res, store_plugin = self.search_pool.get_result() if res: self.results_view.model().add_result(res, store_plugin) - - if not self.checker.isActive(): - if not self.results_view.model().has_results(): - info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False) + + if not self.results_view.model().has_results(): + info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False) def open_store(self, index): From 70da6dd8522c6ec454e5d07a7212408df742db68 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 24 Apr 2011 00:21:28 -0400 Subject: [PATCH 35/77] Store: Fix no books found dialog showing too often. --- src/calibre/gui2/store/search/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 70e92d1756..7ff38c3f31 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -206,7 +206,7 @@ class SearchDialog(QDialog, Ui_Dialog): if res: self.results_view.model().add_result(res, store_plugin) - if not self.results_view.model().has_results(): + if not self.search_pool.threads_running() and not self.results_view.model().has_results(): info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False) From 5374b839ce6873299a369b259597abe5d4f673b3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 24 Apr 2011 11:07:12 +0100 Subject: [PATCH 36/77] Change bool search matching to raise an exception for invalid queries --- src/calibre/library/caches.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index e4342988b8..ca256e0350 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -560,6 +560,10 @@ class ResultCache(SearchQueryParser): # {{{ loc = self.field_metadata[location]['rec_index'] matches = set() query = icu_lower(query) + if query not in (_('no'), _('unchecked'), '_no', 'false', + _('yes'), _('checked'), '_yes', 'true', + _('empty'), _('blank'), '_empty'): + raise ParseException(_('Invalid boolean query "{0}"').format(query)) for id_ in candidates: item = self._data[id_] if item is None: From f4da91d06ab81fc51d5acf494ebc625e20f40507 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 24 Apr 2011 07:33:28 -0400 Subject: [PATCH 37/77] Store: Still open MobileRead dialog even when update cache is canceled. Ensure Cache thread is stopped. --- src/calibre/gui2/store/mobileread/mobileread_plugin.py | 8 ++++---- src/calibre/gui2/store/search/search.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/store/mobileread/mobileread_plugin.py b/src/calibre/gui2/store/mobileread/mobileread_plugin.py index 54242ce0b2..271e34a619 100644 --- a/src/calibre/gui2/store/mobileread/mobileread_plugin.py +++ b/src/calibre/gui2/store/mobileread/mobileread_plugin.py @@ -37,10 +37,10 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): d.set_tags(self.config.get('tags', '')) d.exec_() else: - if self.update_cache(parent, 30): - d = MobeReadStoreDialog(self, parent) - d.setWindowTitle(self.name) - d.exec_() + self.update_cache(parent, 30) + d = MobeReadStoreDialog(self, parent) + d.setWindowTitle(self.name) + d.exec_() def search(self, query, max_results=10, timeout=60): books = self.get_book_list() diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 7ff38c3f31..5654df8ffc 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -247,5 +247,6 @@ class SearchDialog(QDialog, Ui_Dialog): def dialog_closed(self, result): self.results_view.model().closing() self.search_pool.abort() + self.cache_pool.abort() self.save_state() From e690e7196e094787985dd148038d38a6d5e08163 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 24 Apr 2011 09:44:50 -0400 Subject: [PATCH 38/77] Plucker metadata reader. --- src/calibre/ebooks/metadata/pdb.py | 4 +- src/calibre/ebooks/metadata/plucker.py | 73 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/calibre/ebooks/metadata/plucker.py diff --git a/src/calibre/ebooks/metadata/pdb.py b/src/calibre/ebooks/metadata/pdb.py index ddf2b0c818..d01bb0ecdb 100644 --- a/src/calibre/ebooks/metadata/pdb.py +++ b/src/calibre/ebooks/metadata/pdb.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- ''' -Read meta information from eReader pdb files. +Read meta information from pdb files. ''' __license__ = 'GPL v3' @@ -13,10 +13,12 @@ import re from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.pdb.header import PdbHeaderReader from calibre.ebooks.metadata.ereader import get_metadata as get_eReader +from calibre.ebooks.metadata.plucker import get_metadata as get_plucker MREADER = { 'PNPdPPrs' : get_eReader, 'PNRdPPrs' : get_eReader, + 'DataPlkr' : get_plucker, } from calibre.ebooks.metadata.ereader import set_metadata as set_eReader diff --git a/src/calibre/ebooks/metadata/plucker.py b/src/calibre/ebooks/metadata/plucker.py new file mode 100644 index 0000000000..991945f42b --- /dev/null +++ b/src/calibre/ebooks/metadata/plucker.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +''' +Read meta information from Plucker pdb files. +''' + +__license__ = 'GPL v3' +__copyright__ = '2009, John Schember ' +__docformat__ = 'restructuredtext en' + +import struct +from datetime import datetime + +from calibre.ebooks.metadata import MetaInformation +from calibre.ebooks.pdb.header import PdbHeaderReader +from calibre.ebooks.pdb.plucker.reader import SectionHeader, DATATYPE_METADATA, \ + MIBNUM_TO_NAME + +def get_metadata(stream, extract_cover=True): + ''' + Return metadata as a L{MetaInfo} object + ''' + mi = MetaInformation(_('Unknown'), [_('Unknown')]) + stream.seek(0) + + pheader = PdbHeaderReader(stream) + section_data = None + for i in range(1, pheader.num_sections): + raw_data = pheader.section_data(i) + section_header = SectionHeader(raw_data) + if section_header.type == DATATYPE_METADATA: + section_data = raw_data[8:] + break + + if not section_data: + return mi + + default_encoding = 'latin-1' + record_count, = struct.unpack('>H', section_data[0:2]) + adv = 0 + title = None + author = None + pubdate = 0 + for i in xrange(record_count): + type, = struct.unpack('>H', section_data[2+adv:4+adv]) + length, = struct.unpack('>H', section_data[4+adv:6+adv]) + + # CharSet + if type == 1: + val, = struct.unpack('>H', section_data[6+adv:8+adv]) + default_encoding = MIBNUM_TO_NAME.get(val, 'latin-1') + # Author + elif type == 4: + author = section_data[6+adv+(2*length)] + # Title + elif type == 5: + title = section_data[6+adv+(2*length)] + # Publication Date + elif type == 6: + pubdate, = struct.unpack('>I', section_data[6+adv:6+adv+4]) + + adv += 2*length + + if title: + mi.title = title.replace('\0', '').decode(default_encoding, 'replace') + if author: + author = author.replace('\0', '').decode(default_encoding, 'replace') + mi.author = author.split(',') + mi.pubdate = datetime.fromtimestamp(pubdate) + + return mi From 3d1e7fe162ae8af4fb88926c9427e7eb4c126259 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 24 Apr 2011 09:47:14 -0400 Subject: [PATCH 39/77] Store: Fix Baen. Missed refactoring to JSON config. --- src/calibre/gui2/store/baen_webscription_plugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/store/baen_webscription_plugin.py b/src/calibre/gui2/store/baen_webscription_plugin.py index d4f7924851..5be7e9c161 100644 --- a/src/calibre/gui2/store/baen_webscription_plugin.py +++ b/src/calibre/gui2/store/baen_webscription_plugin.py @@ -24,10 +24,9 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog class BaenWebScriptionStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): - settings = self.get_settings() url = 'http://www.webscription.net/' - if external or settings.get(self.name + '_open_external', False): + if external or self.config.get('open_external', False): if detail_item: url = url + detail_item open_url(QUrl(url_slash_cleaner(url))) @@ -37,7 +36,7 @@ class BaenWebScriptionStore(BasicStoreConfig, StorePlugin): detail_url = url + detail_item d = WebStoreDialog(self.gui, url, parent, detail_url) d.setWindowTitle(self.name) - d.set_tags(settings.get(self.name + '_tags', '')) + d.set_tags(self.config.get('tags', '')) d.exec_() def search(self, query, max_results=10, timeout=60): From 27b2b3c31dff54f29a711218746255931846920c Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 24 Apr 2011 09:49:53 -0400 Subject: [PATCH 40/77] Store: Remove unused API call. --- src/calibre/gui2/store/__init__.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/calibre/gui2/store/__init__.py b/src/calibre/gui2/store/__init__.py index c95d794975..fd2fb965a9 100644 --- a/src/calibre/gui2/store/__init__.py +++ b/src/calibre/gui2/store/__init__.py @@ -161,18 +161,6 @@ class StorePlugin(object): # {{{ ''' return False - def get_settings(self): - ''' - This is only useful for plugins that implement - :attr:`config_widget` that is the only way to save - settings. This is used by plugins to get the saved - settings and apply when necessary. - - :return: A dictionary filled with the settings used - by this plugin. - ''' - raise NotImplementedError() - def do_genesis(self): self.genesis() From ea0deda6be5e8d10857cc41fe293cfcb4c391a1f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 08:41:25 -0600 Subject: [PATCH 41/77] Add a callback to apply_metadata --- src/calibre/gui2/actions/edit_metadata.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 1718123435..44212e92a7 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -529,13 +529,17 @@ class EditMetadataAction(InterfaceAction): view.reset() # Apply bulk metadata changes {{{ - def apply_metadata_changes(self, id_map, title=None, msg=''): + def apply_metadata_changes(self, id_map, title=None, msg='', callback=None): ''' Apply the metadata changes in id_map to the database synchronously id_map must be a mapping of ids to Metadata objects. Set any fields you do not want updated in the Metadata object to null. An easy way to do that is to create a metadata object as Metadata(_('Unknown')) and then only set the fields you want changed on this object. + + callback can be either None or a function accepting a single argument, + in which case it is called after applying is complete with the list of + changed ids. ''' if title is None: title = _('Applying changed metadata') @@ -544,6 +548,7 @@ class EditMetadataAction(InterfaceAction): self.apply_failures = [] self.applied_ids = [] self.apply_pd = None + self.apply_callback = callback if len(self.apply_id_map) > 1: from calibre.gui2.dialogs.progress import ProgressDialog self.apply_pd = ProgressDialog(title, msg, min=0, @@ -611,6 +616,11 @@ class EditMetadataAction(InterfaceAction): self.apply_id_map = [] self.apply_pd = None + try: + if callable(self.apply_callback): + self.apply_callback(self.applied_ids) + finally: + self.apply_callback = None # }}} From 046f6bc2e42604ba7f71f648cb3eafbd4f14c09d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 08:46:02 -0600 Subject: [PATCH 42/77] Updated Quick Start Guide --- resources/quick_start.epub | Bin 129993 -> 130557 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/quick_start.epub b/resources/quick_start.epub index 589fd1d0dc6e1c222e00014bfd3162a7fe70bcbb..2d590ebef268689c4d742c29ae161f719efd4f1c 100644 GIT binary patch literal 130557 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 literal 129993 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ˠK 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 From 53b02c676e2a491ee452238b23818f441b36ac3d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 08:59:36 -0600 Subject: [PATCH 43/77] Update Financial Times --- recipes/financial_times.recipe | 1 + 1 file changed, 1 insertion(+) diff --git a/recipes/financial_times.recipe b/recipes/financial_times.recipe index 0e3c91d3e3..e750b6f113 100644 --- a/recipes/financial_times.recipe +++ b/recipes/financial_times.recipe @@ -53,6 +53,7 @@ class FinancialTimes(BasicNewsRecipe): feeds = [ (u'UK' , u'http://www.ft.com/rss/home/uk' ) ,(u'US' , u'http://www.ft.com/rss/home/us' ) + ,(u'Europe' , u'http://www.ft.com/rss/home/europe' ) ,(u'Asia' , u'http://www.ft.com/rss/home/asia' ) ,(u'Middle East', u'http://www.ft.com/rss/home/middleeast') ] From a9e1969712f68f1f94a2710599301203dbccc19a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 11:06:49 -0600 Subject: [PATCH 44/77] Fix #769946 (linux.py script crashes with an OS error 39 (directory not empty)) --- src/calibre/linux.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 5c80df20df..d83bba061f 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -149,7 +149,8 @@ class PostInstall: if islinux or isfreebsd: for f in os.listdir('.'): if os.stat(f).st_uid == 0: - os.rmdir(f) if os.path.isdir(f) else os.unlink(f) + import shutil + shutil.rmtree(f) if os.path.isdir(f) else os.unlink(f) if os.stat(config_dir).st_uid == 0: os.rmdir(config_dir) From 9df06f761cd87e4eb126d477b869c7453f846e27 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 11:18:49 -0600 Subject: [PATCH 45/77] Fix #769944 (Ebook Viewer: Make "Toogle Full Screen" button more accessible) --- src/calibre/gui2/viewer/main.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/viewer/main.ui b/src/calibre/gui2/viewer/main.ui index d470a386c6..04166fe2cf 100644 --- a/src/calibre/gui2/viewer/main.ui +++ b/src/calibre/gui2/viewer/main.ui @@ -121,7 +121,7 @@ - + @@ -130,7 +130,7 @@ - + From 3388d6ffd35727dbc2b1e533112847e9360c230f Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 24 Apr 2011 18:45:12 -0400 Subject: [PATCH 46/77] Fix bug #769489: eReader PDB not specifying all footnotes causes conversion to fail. --- src/calibre/ebooks/pdb/ereader/reader132.py | 12 ++++++++++-- src/calibre/ebooks/pml/pmlconverter.py | 5 ++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/pdb/ereader/reader132.py b/src/calibre/ebooks/pdb/ereader/reader132.py index df98ce15b1..09e4b624e5 100644 --- a/src/calibre/ebooks/pdb/ereader/reader132.py +++ b/src/calibre/ebooks/pdb/ereader/reader132.py @@ -129,14 +129,22 @@ class Reader132(FormatReader): footnoteids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.footnote_offset).decode('cp1252' if self.encoding is None else self.encoding)) for fid, i in enumerate(range(self.header_record.footnote_offset + 1, self.header_record.footnote_offset + self.header_record.footnote_count)): self.log.debug('Extracting footnote page %i' % i) - html += footnote_to_html(footnoteids[fid], self.decompress_text(i)) + if fid < len(footnoteids): + fid = footnoteids[fid] + else: + fid = '' + html += footnote_to_html(fid, self.decompress_text(i)) if self.header_record.sidebar_count > 0: html += '

%s

' % _('Sidebar') sidebarids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.sidebar_offset).decode('cp1252' if self.encoding is None else self.encoding)) for sid, i in enumerate(range(self.header_record.sidebar_offset + 1, self.header_record.sidebar_offset + self.header_record.sidebar_count)): self.log.debug('Extracting sidebar page %i' % i) - html += sidebar_to_html(sidebarids[sid], self.decompress_text(i)) + if sid < len(sidebarids): + sid = sidebarids[sid] + else: + sid = '' + html += sidebar_to_html(sid, self.decompress_text(i)) html += '' diff --git a/src/calibre/ebooks/pml/pmlconverter.py b/src/calibre/ebooks/pml/pmlconverter.py index 89a495cfc6..7bb23946ca 100644 --- a/src/calibre/ebooks/pml/pmlconverter.py +++ b/src/calibre/ebooks/pml/pmlconverter.py @@ -749,7 +749,10 @@ def pml_to_html(pml): def footnote_sidebar_to_html(pre_id, id, pml): id = id.strip('\x01') - html = '

' % (pre_id, id, pml_to_html(pml), pre_id, id) + if id.strip(): + html = '

' % (pre_id, id, pml_to_html(pml), pre_id, id) + else: + html = '

%s
' % pml_to_html(pml) return html def footnote_to_html(id, pml): From befa061c9dfcd03f39523bab67ea8cd3fb6f271c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 17:11:56 -0600 Subject: [PATCH 47/77] Fix #770037 (HTC Desire HD not recognised as device) --- src/calibre/devices/android/driver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 7fe246f450..4a48aef441 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -26,6 +26,7 @@ class ANDROID(USBMS): 0xc92 : [0x100], 0xc97 : [0x226], 0xc99 : [0x0100], + 0xca2 : [0x226], 0xca3 : [0x100], 0xca4 : [0x226], }, From 077ecd7d41a0db158f805cbf2c18b9356e467b95 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 24 Apr 2011 21:15:26 -0400 Subject: [PATCH 48/77] PDB - Plucker: Remove pure python PalmDoc decompression and use C module. --- src/calibre/ebooks/pdb/plucker/reader.py | 28 +----------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 28e875aceb..39ceb33b13 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -109,32 +109,6 @@ MIBNUM_TO_NAME = { 2258: 'cp1258', } -def decompress_doc(data): - buffer = [ord(i) for i in data] - res = [] - i = 0 - while i < len(buffer): - c = buffer[i] - i += 1 - if c >= 1 and c <= 8: - res.extend(buffer[i:i+c]) - i += c - elif c <= 0x7f: - res.append(c) - elif c >= 0xc0: - res.extend( (ord(' '), c^0x80) ) - else: - c = (c << 8) + buffer[i] - i += 1 - di = (c & 0x3fff) >> 3 - j = len(res) - num = (c & ((1 << 3) - 1)) + 3 - - for k in range( num ): - res.append(res[j - di+k]) - - return ''.join([chr(i) for i in res]) - class HeaderRecord(object): ''' Plucker header. PDB record 0. @@ -504,7 +478,7 @@ class Reader(FormatReader): raise NotImplementedError return zlib.decompress(data) elif self.header_record.compression == 1: - #from calibre.ebooks.compression.palmdoc import decompress_doc + from calibre.ebooks.compression.palmdoc import decompress_doc return decompress_doc(data) def process_phtml(self, d, paragraph_offsets=[]): From 978e1f826b50bce4cfd23981cf678a564081bd00 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 19:44:17 -0600 Subject: [PATCH 49/77] Make the book details panel completely user configurable --- resources/default_tweaks.py | 20 - resources/templates/book_details.css | 26 + src/calibre/ebooks/metadata/book/base.py | 8 +- src/calibre/gui2/__init__.py | 12 +- src/calibre/gui2/actions/show_book_details.py | 2 +- src/calibre/gui2/book_details.py | 280 ++++---- src/calibre/gui2/dialogs/book_info.py | 91 +-- src/calibre/gui2/dialogs/book_info.ui | 86 +-- src/calibre/gui2/library/models.py | 149 ++-- src/calibre/gui2/preferences/look_feel.py | 12 +- src/calibre/gui2/preferences/look_feel.ui | 669 +++++++++++------- src/calibre/library/db/__init__.py | 9 - src/calibre/library/db/base.py | 37 - src/calibre/library/field_metadata.py | 20 +- 14 files changed, 717 insertions(+), 704 deletions(-) create mode 100644 resources/templates/book_details.css delete mode 100644 src/calibre/library/db/__init__.py delete mode 100644 src/calibre/library/db/base.py diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 091aa9a34d..08017b5c98 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -266,26 +266,6 @@ max_content_server_tags_shown=5 content_server_will_display = ['*'] content_server_wont_display = [] -#: Set custom metadata fields that the book details panel will or will not display. -# book_details_will_display is a list of custom fields to be displayed. -# book_details_wont_display is a list of custom fields not to be displayed. -# wont_display has priority over will_display. -# The special value '*' means all custom fields. The value [] means no entries. -# Defaults: -# book_details_will_display = ['*'] -# book_details_wont_display = [] -# Examples: -# To display only the custom fields #mytags and #genre: -# book_details_will_display = ['#mytags', '#genre'] -# book_details_wont_display = [] -# To display all fields except #mycomments: -# book_details_will_display = ['*'] -# book_details_wont_display['#mycomments'] -# As above, this tweak affects only display of custom fields. The standard -# fields are not affected -book_details_will_display = ['*'] -book_details_wont_display = [] - #: Set the maximum number of sort 'levels' # Set the maximum number of sort 'levels' that calibre will use to resort the # library after certain operations such as searches or device insertion. Each diff --git a/resources/templates/book_details.css b/resources/templates/book_details.css new file mode 100644 index 0000000000..9671ad77ee --- /dev/null +++ b/resources/templates/book_details.css @@ -0,0 +1,26 @@ +a { + text-decoration: none; + color: blue +} +.comments { + margin-top: 0; + padding-top: 0; + text-indent: 0 +} + +table.fields { + margin-bottom: 0; + padding-bottom: 0; +} + +table.fields td { + vertical-align: top +} + +table.fields td.title { + font-weight: bold +} + +.series_name { + font-style: italic +} diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index faac8e98b1..5f1841d518 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -19,6 +19,9 @@ from calibre.utils.date import isoformat, format_date from calibre.utils.icu import sort_key from calibre.utils.formatter import TemplateFormatter +def human_readable(size, precision=2): + """ Convert a size in bytes into megabytes """ + return ('%.'+str(precision)+'f'+ 'MB') % ((size/(1024.*1024.)),) NULL_VALUES = { 'user_metadata': {}, @@ -551,7 +554,8 @@ class Metadata(object): def format_field_extended(self, key, series_with_index=True): from calibre.ebooks.metadata import authors_to_string ''' - returns the tuple (field_name, formatted_value) + returns the tuple (field_name, formatted_value, original_value, + field_metadata) ''' # Handle custom series index @@ -627,6 +631,8 @@ class Metadata(object): res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) elif datatype == 'rating': res = res/2.0 + elif key in ('book_size', 'size'): + res = human_readable(res) return (name, unicode(res), orig_res, fmeta) return (None, None, None, None) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index de066359ed..f1357728ec 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -80,6 +80,14 @@ gprefs.defaults['font'] = None gprefs.defaults['tags_browser_partition_method'] = 'first letter' gprefs.defaults['tags_browser_collapse_at'] = 100 gprefs.defaults['edit_metadata_single_layout'] = 'default' +gprefs.defaults['book_display_fields'] = [ + ('title', False), ('authors', False), ('formats', True), + ('series', True), ('identifiers', True), ('tags', True), + ('path', True), ('publisher', False), ('rating', False), + ('author_sort', False), ('sort', False), ('timestamp', False), + ('uuid', False), ('comments', True), ('id', False), ('pubdate', False), + ('last_modified', False), ('size', False), + ] # }}} @@ -89,7 +97,7 @@ UNDEFINED_QDATE = QDate(UNDEFINED_DATE) ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher', 'tags', 'series', 'pubdate'] -def _config(): +def _config(): # {{{ c = Config('gui', 'preferences for the calibre GUI') c.add_opt('send_to_storage_card_by_default', default=False, help=_('Send file to storage card instead of main memory by default')) @@ -181,6 +189,8 @@ def _config(): return ConfigProxy(c) config = _config() +# }}} + # Turn off DeprecationWarnings in windows GUI if iswindows: import warnings diff --git a/src/calibre/gui2/actions/show_book_details.py b/src/calibre/gui2/actions/show_book_details.py index 11064f2f39..1c28a08a79 100644 --- a/src/calibre/gui2/actions/show_book_details.py +++ b/src/calibre/gui2/actions/show_book_details.py @@ -30,5 +30,5 @@ class ShowBookDetailsAction(InterfaceAction): index = self.gui.library_view.currentIndex() if index.isValid(): BookInfo(self.gui, self.gui.library_view, index, - self.gui.iactions['View'].view_format_by_id).show() + self.gui.book_details.handle_click).show() diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 4e75a42e89..f2f048a5d5 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -5,67 +5,151 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import collections, sys -from Queue import Queue -from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \ - QPropertyAnimation, QEasingCurve, QThread, QApplication, QFontInfo, \ - QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu +from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, + QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, + QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu) from PyQt4.QtWebKit import QWebView -from calibre import fit_image, prepare_string_for_xml -from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \ - IMAGE_EXTENSIONS, dnd_has_extension +from calibre import fit_image, force_unicode, prepare_string_for_xml +from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files, + IMAGE_EXTENSIONS, dnd_has_extension) from calibre.ebooks import BOOK_EXTENSIONS -from calibre.constants import preferred_encoding +from calibre.ebooks.metadata.book.base import (field_metadata, Metadata) +from calibre.ebooks.metadata import fmt_sidx +from calibre.constants import filesystem_encoding from calibre.library.comments import comments_to_html -from calibre.gui2 import config, open_local_file, open_url, pixmap_to_data +from calibre.gui2 import (config, open_local_file, open_url, pixmap_to_data, + gprefs) from calibre.utils.icu import sort_key -# render_rows(data) {{{ -WEIGHTS = collections.defaultdict(lambda : 100) -WEIGHTS[_('Path')] = 5 -WEIGHTS[_('Formats')] = 1 -WEIGHTS[_('Collections')] = 2 -WEIGHTS[_('Series')] = 3 -WEIGHTS[_('Tags')] = 4 -def render_rows(data): - keys = data.keys() - # First sort by name. The WEIGHTS sort will preserve this sub-order - keys.sort(key=sort_key) - keys.sort(key=lambda x: WEIGHTS[x]) - rows = [] - for key in keys: - txt = data[key] - if key in ('id', _('Comments')) or not hasattr(txt, 'strip') or not txt.strip() or \ - txt == 'None': +def render_html(mi, css, vertical, widget, all_fields=False): # {{{ + table = render_data(mi, all_fields=all_fields, + use_roman_numbers=config['use_roman_numerals_for_series_number']) + + def color_to_string(col): + ans = '#000000' + if col.isValid(): + col = col.toRgb() + if col.isValid(): + ans = unicode(col.name()) + return ans + + f = QFontInfo(QApplication.font(widget)).pixelSize() + c = color_to_string(QApplication.palette().color(QPalette.Normal, + QPalette.WindowText)) + templ = u'''\ + + + + + + + %%s + + + '''%(f, c, css) + comments = u'' + if mi.comments: + comments = comments_to_html(force_unicode(mi.comments)) + right_pane = u'
%s
'%comments + + if vertical: + ans = templ%(table+right_pane) + else: + ans = templ%(u'
%s%s
' + % (table, right_pane)) + return ans + +def get_field_list(fm): + fieldlist = list(gprefs['book_display_fields']) + names = frozenset([x[0] for x in fieldlist]) + for field in fm.displayable_field_keys(): + if field not in names: + fieldlist.append((field, True)) + return fieldlist + +def render_data(mi, use_roman_numbers=True, all_fields=False): + ans = [] + isdevice = not hasattr(mi, 'id') + fm = getattr(mi, 'field_metadata', field_metadata) + + for field, display in get_field_list(fm): + metadata = fm.get(field, None) + if all_fields: + display = True + if (not display or not metadata or mi.is_null(field) or + field == 'comments'): continue - if isinstance(key, str): - key = key.decode(preferred_encoding, 'replace') - if isinstance(txt, str): - txt = txt.decode(preferred_encoding, 'replace') - if key.endswith(u':html'): - key = key[:-5] - txt = comments_to_html(txt) - elif '' not in txt: - txt = prepare_string_for_xml(txt) - if 'id' in data: - if key == _('Path'): - txt = u'%s'%(data['id'], - txt, _('Click to open')) - if key == _('Formats') and txt and txt != _('None'): - fmts = [x.strip() for x in txt.split(',')] - fmts = [u'%s' % (data['id'], x, x) for x - in fmts] - txt = ', '.join(fmts) + name = metadata['name'] + if not name: + name = field + name += ':' + if metadata['datatype'] == 'comments': + val = getattr(mi, field) + if val: + val = force_unicode(val) + ans.append((field, + u'%s'%comments_to_html(val))) + elif field == 'path': + if mi.path: + path = force_unicode(mi.path, filesystem_encoding) + scheme = u'devpath' if isdevice else u'path' + url = prepare_string_for_xml(path if isdevice else + unicode(mi.id), True) + link = u'%s' % (scheme, url, + prepare_string_for_xml(path, True), _('Click to open')) + ans.append((field, u'%s%s'%(name, link))) + elif field == 'formats': + if isdevice: continue + fmts = [u'%s' % (mi.id, x, x) for x + in mi.formats] + ans.append((field, u'%s%s'%(name, + u', '.join(fmts)))) + elif field == 'identifiers': + pass # TODO else: - if key == _('Path'): - txt = u'%s'%(txt, - _('Click to open')) + val = mi.format_field(field)[-1] + if val is None: + continue + val = prepare_string_for_xml(val) + if metadata['datatype'] == 'series': + if metadata['is_custom']: + sidx = mi.get_extra(field) + else: + sidx = getattr(mi, field+'_index') + if sidx is None: + sidx = 1.0 + val = _('Book %s of %s')%(fmt_sidx(sidx, + use_roman=use_roman_numbers), + prepare_string_for_xml(getattr(mi, field))) - rows.append((key, txt)) - return rows + ans.append((field, u'%s%s'%(name, val))) + + dc = getattr(mi, 'device_collections', []) + if dc: + dc = u', '.join(sorted(dc, key=sort_key)) + ans.append(('device_collections', + u'%s%s'%( + _('Collections')+':', dc))) + + def classname(field): + try: + dt = fm[field]['datatype'] + except: + dt = 'text' + return 'datatype_%s'%dt + + ans = [u'%s'%(field.replace('#', '_'), + classname(field), html) for field, html in ans] + # print '\n'.join(ans) + return u'%s
'%(u'\n'.join(ans)) # }}} @@ -117,10 +201,10 @@ class CoverView(QWidget): # {{{ def show_data(self, data): self.animation.stop() - same_item = data.get('id', True) == self.data.get('id', False) + same_item = getattr(data, 'id', True) == self.data.get('id', False) self.data = {'id':data.get('id', None)} - if data.has_key('cover'): - self.pixmap = QPixmap.fromImage(data.pop('cover')) + if data.cover_data[1]: + self.pixmap = QPixmap.fromImage(data.cover_data[1]) if self.pixmap.isNull() or self.pixmap.width() < 5 or \ self.pixmap.height() < 5: self.pixmap = self.default_pixmap @@ -188,32 +272,6 @@ class CoverView(QWidget): # {{{ # Book Info {{{ -class RenderComments(QThread): - - rdone = pyqtSignal(object, object) - - def __init__(self, parent): - QThread.__init__(self, parent) - self.queue = Queue() - self.start() - - def run(self): - while True: - try: - rows, comments = self.queue.get() - except: - break - import time - time.sleep(0.001) - oint = sys.getcheckinterval() - sys.setcheckinterval(5) - try: - self.rdone.emit(rows, comments_to_html(comments)) - except: - pass - sys.setcheckinterval(oint) - - class BookInfo(QWebView): link_clicked = pyqtSignal(object) @@ -221,8 +279,6 @@ class BookInfo(QWebView): def __init__(self, vertical, parent=None): QWebView.__init__(self, parent) self.vertical = vertical - self.renderer = RenderComments(self) - self.renderer.rdone.connect(self._show_data, type=Qt.QueuedConnection) self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks) self.linkClicked.connect(self.link_activated) self._link_clicked = False @@ -231,6 +287,7 @@ class BookInfo(QWebView): self.setAcceptDrops(False) palette.setBrush(QPalette.Base, Qt.transparent) self.page().setPalette(palette) + self.css = P('templates/book_details.css', data=True).decode('utf-8') def link_activated(self, link): self._link_clicked = True @@ -240,56 +297,9 @@ class BookInfo(QWebView): def turnoff_scrollbar(self, *args): self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) - def show_data(self, data): - rows = render_rows(data) - rows = u'\n'.join([u'%s:%s'%(k,t) for - k, t in rows]) - comments = data.get(_('Comments'), '') - if not comments or comments == u'None': - comments = '' - self.renderer.queue.put((rows, comments)) - self._show_data(rows, '') - - - def _show_data(self, rows, comments): - - def color_to_string(col): - ans = '#000000' - if col.isValid(): - col = col.toRgb() - if col.isValid(): - ans = unicode(col.name()) - return ans - - f = QFontInfo(QApplication.font(self.parent())).pixelSize() - c = color_to_string(QApplication.palette().color(QPalette.Normal, - QPalette.WindowText)) - templ = u'''\ - - - - - - %%s - - - '''%(f, c) - if self.vertical: - extra = '' - if comments: - extra = u'
%s
'%comments - self.setHtml(templ%(u'%s
%s'%(rows, extra))) - else: - left_pane = u'%s
'%rows - right_pane = u'
%s
'%comments - self.setHtml(templ%(u'
%s%s
' - % (left_pane, right_pane))) + def show_data(self, mi): + html = render_html(mi, self.css, self.vertical, self.parent()) + self.setHtml(html) def mouseDoubleClickEvent(self, ev): swidth = self.page().mainFrame().scrollBarGeometry(Qt.Vertical).width() @@ -457,10 +467,10 @@ class BookDetails(QWidget): # {{{ self._layout.addWidget(self.cover_view) self.book_info = BookInfo(vertical, self) self._layout.addWidget(self.book_info) - self.book_info.link_clicked.connect(self._link_clicked) + self.book_info.link_clicked.connect(self.handle_click) self.setCursor(Qt.PointingHandCursor) - def _link_clicked(self, link): + def handle_click(self, link): typ, _, val = link.partition(':') if typ == 'path': self.open_containing_folder.emit(int(val)) @@ -484,7 +494,7 @@ class BookDetails(QWidget): # {{{ def show_data(self, data): self.book_info.show_data(data) self.cover_view.show_data(data) - self.current_path = data.get(_('Path'), '') + self.current_path = getattr(data, u'path', u'') self.update_layout() def update_layout(self): @@ -500,7 +510,7 @@ class BookDetails(QWidget): # {{{ ) def reset_info(self): - self.show_data({}) + self.show_data(Metadata(_('Unknown'))) # }}} diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 46d26c2f4a..4036e71a38 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -3,30 +3,33 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' -import textwrap, os, re -from PyQt4.Qt import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, \ - QDialog, QPixmap, QIcon, QSize +from PyQt4.Qt import (QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, + QDialog, QPixmap, QIcon, QSize, QPalette) from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo -from calibre.gui2 import dynamic, open_local_file, open_url +from calibre.gui2 import dynamic from calibre import fit_image -from calibre.library.comments import comments_to_html -from calibre.utils.icu import sort_key - +from calibre.gui2.book_details import render_html class BookInfo(QDialog, Ui_BookInfo): - def __init__(self, parent, view, row, view_func): + def __init__(self, parent, view, row, link_delegate): QDialog.__init__(self, parent) Ui_BookInfo.__init__(self) self.setupUi(self) self.gui = parent self.cover_pixmap = None - self.comments.sizeHint = self.comments_size_hint - self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks) - self.comments.linkClicked.connect(self.link_clicked) - self.view_func = view_func + self.details.sizeHint = self.details_size_hint + self.details.page().setLinkDelegationPolicy(self.details.page().DelegateAllLinks) + self.details.linkClicked.connect(self.link_clicked) + self.css = P('templates/book_details.css', data=True).decode('utf-8') + self.link_delegate = link_delegate + self.details.setAttribute(Qt.WA_OpaquePaintEvent, False) + palette = self.details.palette() + self.details.setAcceptDrops(False) + palette.setBrush(QPalette.Base, Qt.transparent) + self.details.page().setPalette(palette) self.view = view @@ -37,7 +40,6 @@ class BookInfo(QDialog, Ui_BookInfo): self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave) self.connect(self.next_button, SIGNAL('clicked()'), self.next) self.connect(self.previous_button, SIGNAL('clicked()'), self.previous) - self.connect(self.text, SIGNAL('linkActivated(QString)'), self.open_book_path) self.fit_cover.stateChanged.connect(self.toggle_cover_fit) self.cover.resizeEvent = self.cover_view_resized self.cover.cover_changed.connect(self.cover_changed) @@ -46,6 +48,10 @@ class BookInfo(QDialog, Ui_BookInfo): screen_height = desktop.availableGeometry().height() - 100 self.resize(self.size().width(), screen_height) + def link_clicked(self, qurl): + link = unicode(qurl.toString()) + self.link_delegate(link) + def cover_changed(self, data): if self.current_row is not None: id_ = self.view.model().id(self.current_row) @@ -60,11 +66,8 @@ class BookInfo(QDialog, Ui_BookInfo): if self.fit_cover.isChecked(): self.resize_cover() - def link_clicked(self, url): - open_url(url) - - def comments_size_hint(self): - return QSize(350, 250) + def details_size_hint(self): + return QSize(350, 550) def toggle_cover_fit(self, state): dynamic.set('book_info_dialog_fit_cover', self.fit_cover.isChecked()) @@ -77,13 +80,6 @@ class BookInfo(QDialog, Ui_BookInfo): row = current.row() self.refresh(row) - def open_book_path(self, path): - path = unicode(path) - if os.sep in path: - open_local_file(path) - else: - self.view_func(self.view.model().id(self.current_row), path) - def next(self): row = self.view.currentIndex().row() ni = self.view.model().index(row+1, 0) @@ -117,8 +113,8 @@ class BookInfo(QDialog, Ui_BookInfo): row = row.row() if row == self.current_row: return - info = self.view.model().get_book_info(row) - if info is None: + mi = self.view.model().get_book_display_info(row) + if mi is None: # Indicates books was deleted from library, or row numbers have # changed return @@ -126,40 +122,11 @@ class BookInfo(QDialog, Ui_BookInfo): self.previous_button.setEnabled(False if row == 0 else True) self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True) self.current_row = row - self.setWindowTitle(info[_('Title')]) - self.title.setText(''+info.pop(_('Title'))) - comments = info.pop(_('Comments'), '') - if comments: - comments = comments_to_html(comments) - if re.search(r'<[a-zA-Z]+>', comments) is None: - lines = comments.splitlines() - lines = [x if x.strip() else '

' for x in lines] - comments = '\n'.join(lines) - self.comments.setHtml('
%s
' % comments) - self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks) - cdata = info.pop('cover', '') - self.cover_pixmap = QPixmap.fromImage(cdata) + self.setWindowTitle(mi.title) + self.title.setText(''+mi.title) + mi.title = _('Unknown') + self.cover_pixmap = QPixmap.fromImage(mi.cover_data[1]) self.resize_cover() + html = render_html(mi, self.css, True, self, all_fields=True) + self.details.setHtml(html) - rows = u'' - self.text.setText('') - self.data = info - if _('Path') in info.keys(): - p = info[_('Path')] - info[_('Path')] = '%s'%(p, p) - if _('Formats') in info.keys(): - formats = info[_('Formats')].split(',') - info[_('Formats')] = '' - for f in formats: - f = f.strip() - info[_('Formats')] += '%s, '%(f,f) - for key in sorted(info.keys(), key=sort_key): - if key == 'id': continue - txt = info[key] - if key.endswith(':html'): - key = key[:-5] - txt = comments_to_html(txt) - if key != _('Path'): - txt = u'
\n'.join(textwrap.wrap(txt, 120)) - rows += u'%s:%s'%(key, txt) - self.text.setText(u''+rows+'
') diff --git a/src/calibre/gui2/dialogs/book_info.ui b/src/calibre/gui2/dialogs/book_info.ui index 9e9e71eda0..44fd1adf22 100644 --- a/src/calibre/gui2/dialogs/book_info.ui +++ b/src/calibre/gui2/dialogs/book_info.ui @@ -20,6 +20,12 @@ + + + 0 + 0 + + 75 @@ -34,82 +40,17 @@ - + - - - - QFrame::NoFrame - - - true - - - - - 0 - 0 - 435 - 670 - - - - - - - TextLabel - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - - - - - Comments - - - - - - - 0 - 0 - - - - - 350 - 16777215 - - - - - about:blank - - - - - - - - - - - - + Fit &cover within view - + @@ -135,6 +76,15 @@ + + + + + about:blank + + + +
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 0bd3f2133a..8b830d2ec2 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -8,20 +8,20 @@ __docformat__ = 'restructuredtext en' import shutil, functools, re, os, traceback from contextlib import closing -from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \ - QModelIndex, QVariant, QDate, QColor +from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, + QModelIndex, QVariant, QDate, QColor) -from calibre.gui2 import NONE, config, UNDEFINED_QDATE +from calibre.gui2 import NONE, UNDEFINED_QDATE from calibre.utils.pyparsing import ParseException from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import tweaks, prefs -from calibre.utils.date import dt_factory, qt_to_dt, isoformat +from calibre.utils.date import dt_factory, qt_to_dt from calibre.utils.icu import sort_key from calibre.utils.search_query_parser import SearchQueryParser -from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ - REGEXP_MATCH, MetadataBackup, force_to_bool -from calibre import strftime, isbytestring, prepare_string_for_xml +from calibre.library.caches import (_match, CONTAINS_MATCH, EQUALS_MATCH, + REGEXP_MATCH, MetadataBackup, force_to_bool) +from calibre import strftime, isbytestring from calibre.constants import filesystem_encoding, DEBUG from calibre.gui2.library import DEFAULT_SORT @@ -114,7 +114,7 @@ class BooksModel(QAbstractTableModel): # {{{ return cc_label in self.custom_columns def read_config(self): - self.use_roman_numbers = config['use_roman_numerals_for_series_number'] + pass def set_device_connected(self, is_connected): self.device_connected = is_connected @@ -355,63 +355,13 @@ class BooksModel(QAbstractTableModel): # {{{ return self.rowCount(None) def get_book_display_info(self, idx): - def custom_keys_to_display(): - ans = getattr(self, '_custom_fields_in_book_info', None) - if ans is None: - cfkeys = set(self.db.custom_field_keys()) - yes_fields = set(tweaks['book_details_will_display']) - no_fields = set(tweaks['book_details_wont_display']) - if '*' in yes_fields: - yes_fields = cfkeys - if '*' in no_fields: - no_fields = cfkeys - ans = frozenset(yes_fields - no_fields) - setattr(self, '_custom_fields_in_book_info', ans) - return ans - - data = {} - cdata = self.cover(idx) - if cdata: - data['cover'] = cdata - tags = list(self.db.get_tags(self.db.id(idx))) - if tags: - tags.sort(key=sort_key) - tags = ', '.join(tags) - else: - tags = _('None') - data[_('Tags')] = tags - formats = self.db.formats(idx) - if formats: - formats = formats.replace(',', ', ') - else: - formats = _('None') - data[_('Formats')] = formats - data[_('Path')] = self.db.abspath(idx) - data['id'] = self.id(idx) - comments = self.db.comments(idx) - if not comments: - comments = _('None') - data[_('Comments')] = comments - series = self.db.series(idx) - if series: - sidx = self.db.series_index(idx) - sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers) - data[_('Series')] = \ - _('Book %s of %s.')%\ - (sidx, prepare_string_for_xml(series)) mi = self.db.get_metadata(idx) - cf_to_display = custom_keys_to_display() - for key in mi.custom_field_keys(): - if key not in cf_to_display: - continue - name, val = mi.format_field(key) - if mi.metadata_for_field(key)['datatype'] == 'comments': - name += ':html' - if val and name not in data: - data[name] = val - - return data - + mi.size = mi.book_size + mi.cover_data = ('jpg', self.cover(idx)) + mi.id = self.db.id(idx) + mi.field_metadata = self.db.field_metadata + mi.path = self.db.abspath(idx, create_dirs=False) + return mi def current_changed(self, current, previous, emit_signal=True): if current.isValid(): @@ -425,16 +375,8 @@ class BooksModel(QAbstractTableModel): # {{{ def get_book_info(self, index): if isinstance(index, int): index = self.index(index, 0) + # If index is not valid returns None data = self.current_changed(index, None, False) - if data is None: - return data - row = index.row() - data[_('Title')] = self.db.title(row) - au = self.db.authors(row) - if not au: - au = _('Unknown') - au = authors_to_string([a.strip().replace('|', ',') for a in au.split(',')]) - data[_('Author(s)')] = au return data def metadata_for(self, ids): @@ -1189,39 +1131,46 @@ class DeviceBooksModel(BooksModel): # {{{ img = self.default_image return img - def current_changed(self, current, previous): - data = {} - item = self.db[self.map[current.row()]] - cover = self.cover(current.row()) - if cover is not self.default_image: - data['cover'] = cover - type = _('Unknown') + def get_book_display_info(self, idx): + from calibre.ebooks.metadata.book.base import Metadata + item = self.db[self.map[idx]] + cover = self.cover(idx) + if cover is self.default_image: + cover = None + title = item.title + if not title: + title = _('Unknown') + au = item.authors + if not au: + au = [_('Unknown')] + mi = Metadata(title, au) + mi.cover_data = ('jpg', cover) + fmt = _('Unknown') ext = os.path.splitext(item.path)[1] if ext: - type = ext[1:].lower() - data[_('Format')] = type - data[_('Path')] = item.path + fmt = ext[1:].lower() + mi.formats = [fmt] + mi.path = (item.path if item.path else None) dt = dt_factory(item.datetime, assume_utc=True) - data[_('Timestamp')] = isoformat(dt, sep=' ', as_utc=False) - data[_('Collections')] = ', '.join(item.device_collections) - - tags = getattr(item, 'tags', None) - if tags: - tags = u', '.join(tags) - else: - tags = _('None') - data[_('Tags')] = tags - comments = getattr(item, 'comments', None) - if not comments: - comments = _('None') - data[_('Comments')] = comments + mi.timestamp = dt + mi.device_collections = list(item.device_collections) + mi.tags = list(getattr(item, 'tags', [])) + mi.comments = getattr(item, 'comments', None) series = getattr(item, 'series', None) if series: sidx = getattr(item, 'series_index', 0) - sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers) - data[_('Series')] = _('Book %s of %s.')%(sidx, series) + mi.series = series + mi.series_index = sidx + return mi - self.new_bookdisplay_data.emit(data) + def current_changed(self, current, previous, emit_signal=True): + if current.isValid(): + idx = current.row() + data = self.get_book_display_info(idx) + if emit_signal: + self.new_bookdisplay_data.emit(data) + else: + return data def paths(self, rows): return [self.db[self.map[r.row()]].path for r in rows ] @@ -1281,7 +1230,7 @@ class DeviceBooksModel(BooksModel): # {{{ elif cname == 'authors': au = self.db[self.map[row]].authors if not au: - au = self.unknown + au = [_('Unknown')] return QVariant(authors_to_string(au)) elif cname == 'size': size = self.db[self.map[row]].size diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 9f06d9a6ab..ed4312ad86 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -5,16 +5,22 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog +from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, + QAbstractListModel) from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2 import config, gprefs, qt_app -from calibre.utils.localization import available_translations, \ - get_language, get_lang +from calibre.utils.localization import (available_translations, + get_language, get_lang) from calibre.utils.config import prefs from calibre.utils.icu import sort_key +class DisplayedFields(QAbstractListModel): + + def __init__(self, parent=None): + QAbstractListModel.__init__(self, parent) + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 996caeb653..2d5409271c 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -14,280 +14,421 @@ Form - - - - User Interface &layout (needs restart): - - - opt_gui_layout - - - - - - - - 250 - 16777215 - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 20 - - - - - - - &Number of covers to show in browse mode (needs restart): - - - opt_cover_flow_queue_length - - - - - - - - - - Choose &language (requires restart): - - - opt_language - - - - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 20 - - - - - - - Show &average ratings in the tags browser - - - true - - - - - - - Disable all animations. Useful if you have a slow/old computer. - - - Disable &animations - - - - - - - Enable system &tray icon (needs restart) - - - - - - - Show &splash screen at startup - - - - - - - Disable &notifications in system tray - - - - - - - Use &Roman numerals for series - - - true - - - - - - - Show cover &browser in a separate window (needs restart) - - - - - - - - - Tags browser category &partitioning method: - - - opt_tags_browser_partition_method - - - - - - - Choose how tag browser subcategories are displayed when + + + + + + 0 + 0 + 682 + 254 + + + + Main interface + + + + + + User Interface &layout (needs restart): + + + opt_gui_layout + + + + + + + + 250 + 16777215 + + + + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + 20 + + + + + + + Choose &language (requires restart): + + + opt_language + + + + + + + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + 20 + + + + + + + Disable all animations. Useful if you have a slow/old computer. + + + Disable &animations + + + + + + + Show &splash screen at startup + + + + + + + &Toolbar + + + + + + + + + &Icon size: + + + opt_toolbar_icon_size + + + + + + + + + + Show &text under icons: + + + opt_toolbar_text + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Interface font: + + + font_display + + + + + + + true + + + + + + + + + Change &font (needs restart) + + + + + + + Enable system &tray icon (needs restart) + + + + + + + Disable &notifications in system tray + + + + + + + + + 0 + 0 + 699 + 151 + + + + Tag Browser + + + + + + Show &average ratings in the tags browser + + + true + + + + + + + + + Tags browser category &partitioning method: + + + opt_tags_browser_partition_method + + + + + + + Choose how tag browser subcategories are displayed when there are more items than the limit. Select by first letter to see an A, B, C list. Choose partitioned to have a list of fixed-sized groups. Set to disabled if you never want subcategories - - - - - - - &Collapse when more items than: - - - opt_tags_browser_collapse_at - - - - - - - If a Tag Browser category has more than this number of items, it is divided + + + + + + + &Collapse when more items than: + + + opt_tags_browser_collapse_at + + + + + + + If a Tag Browser category has more than this number of items, it is divided up into sub-categories. If the partition method is set to disable, this value is ignored. - - - 10000 - - - - - - - Qt::Horizontal - - - - 20 - 5 - - - - - - - - - - Categories with &hierarchical items: - - - opt_categories_using_hierarchy - - - - - - - A comma-separated list of columns in which items containing + + + 10000 + + + + + + + Qt::Horizontal + + + + 20 + 5 + + + + + + + + + + Categories with &hierarchical items: + + + opt_categories_using_hierarchy + + + + + + + A comma-separated list of columns in which items containing periods are displayed in the tag browser trees. For example, if this box contains 'tags' then tags of the form 'Mystery.English' and 'Mystery.Thriller' will be displayed with English and Thriller both under 'Mystery'. If 'tags' is not in this box, then the tags will be displayed each on their own line. - + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 0 + 699 + 306 + + + + Cover Browser + + + + + + Show cover &browser in a separate window (needs restart) + + + + + + + &Number of covers to show in browse mode (needs restart): + + + opt_cover_flow_queue_length + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 0 + 699 + 306 + + + + Book Details + + + + + + Use &Roman numerals for series + + + true + + + + + + + Select displayed metadata + + + + + + true + + + + + + + Move up + + + + :/images/arrow-up.png:/images/arrow-up.png + + + + + + + Move down + + + + :/images/arrow-down.png:/images/arrow-down.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + - - - - &Toolbar - - - - - - - - - &Icon size: - - - opt_toolbar_icon_size - - - - - - - - - - Show &text under icons: - - - opt_toolbar_text - - - - - - - - - - - - Interface font: - - - font_display - - - - - - - true - - - - - - - - - Change &font (needs restart) - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -297,6 +438,8 @@ then the tags will be displayed each on their own line.
calibre/gui2/complete.h
- + + + diff --git a/src/calibre/library/db/__init__.py b/src/calibre/library/db/__init__.py deleted file mode 100644 index 0080175bfa..0000000000 --- a/src/calibre/library/db/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai - -__license__ = 'GPL v3' -__copyright__ = '2010, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - - - diff --git a/src/calibre/library/db/base.py b/src/calibre/library/db/base.py deleted file mode 100644 index a2374583eb..0000000000 --- a/src/calibre/library/db/base.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai - -__license__ = 'GPL v3' -__copyright__ = '2010, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - - -''' Design documentation {{{ - - Storage paradigm {{{ - * Agnostic to storage paradigm (i.e. no book per folder assumptions) - * Two separate concepts: A store and collection - A store is a backend, like a sqlite database associated with a path on - the local filesystem, or a cloud based storage solution. - A collection is a user defined group of stores. Most of the logic for - data manipulation sorting/searching/restrictions should be in the collection - class. The collection class should transparently handle the - conversion from store name + id to row number in the collection. - * Not sure how feasible it is to allow many-many maps between stores - and collections. - }}} - - Event system {{{ - * Comprehensive event system that other components can subscribe to - * Subscribers should be able to temporarily block receiving events - * Should event dispatch be asynchronous? - * Track last modified time for metadata and each format - }}} -}}}''' - -# Imports {{{ -# }}} - - - - diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 374505c467..0ae4d74242 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -188,7 +188,7 @@ class FieldMetadata(dict): 'datatype':'text', 'is_multiple':None, 'kind':'field', - 'name':None, + 'name':_('Author Sort'), 'search_terms':['author_sort'], 'is_custom':False, 'is_category':False, @@ -238,7 +238,7 @@ class FieldMetadata(dict): 'datatype':'datetime', 'is_multiple':None, 'kind':'field', - 'name':_('Date'), + 'name':_('Modified'), 'search_terms':['last_modified'], 'is_custom':False, 'is_category':False, @@ -258,7 +258,7 @@ class FieldMetadata(dict): 'datatype':'text', 'is_multiple':None, 'kind':'field', - 'name':None, + 'name':_('Path'), 'search_terms':[], 'is_custom':False, 'is_category':False, @@ -308,7 +308,7 @@ class FieldMetadata(dict): 'datatype':'float', 'is_multiple':None, 'kind':'field', - 'name':_('Size (MB)'), + 'name':_('Size'), 'search_terms':['size'], 'is_custom':False, 'is_category':False, @@ -399,6 +399,13 @@ class FieldMetadata(dict): if self._tb_cats[k]['kind']=='field' and self._tb_cats[k]['datatype'] is not None] + def displayable_field_keys(self): + return [k for k in self._tb_cats.keys() + if self._tb_cats[k]['kind']=='field' and + self._tb_cats[k]['datatype'] is not None and + k not in ('au_map', 'marked', 'ondevice', 'cover') and + not self.is_series_index(k)] + def standard_field_keys(self): return [k for k in self._tb_cats.keys() if self._tb_cats[k]['kind']=='field' and @@ -442,6 +449,11 @@ class FieldMetadata(dict): def is_custom_field(self, key): return key.startswith(self.custom_field_prefix) + def is_series_index(self, key): + m = self[key] + return (m['datatype'] == 'float' and key.endswith('_index') and + key[:-6] in self) + def key_to_label(self, key): if 'label' not in self._tb_cats[key]: return key From 1acc3716b6ee42aa2bbd16396bc17a369fe2bcaf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 19:47:09 -0600 Subject: [PATCH 50/77] ... --- resources/templates/book_details.css | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/resources/templates/book_details.css b/resources/templates/book_details.css index 9671ad77ee..5059a8f4c3 100644 --- a/resources/templates/book_details.css +++ b/resources/templates/book_details.css @@ -24,3 +24,18 @@ table.fields td.title { .series_name { font-style: italic } + +/* +The HTML that this styleshhet applies to looks like this: + + + + + + +
Formats:EPUB, LIT
Series:Book II of The Sea Beggars
Tags:Fantasy, Fiction
Path:Click to open
+ +

From Publishers Weekly

At the start of Kearney's rousing sequel to The Mark of Ran (2005), Rol Cortishane, the youthful captain of the privateer Revenant, captures a slaver and frees its chained slaves. Back in the harbor of Ganesh Ka in the land of Umer, Rol encounters an untrustworthy acquaintance he hasn't seen in years, Canker, a former king of thieves, who urges Rol to join in the fight to save Rowen, a darkly beautiful queen, whose throne is at risk in mountainous Bionar. That Rowen is Rol's half-sister for whom he has lusted in the past doesn't make Rol's decision to help an easy one. If as in The Mark of Ran the action is more lively at sea than on land, Kearney's solid storytelling and nautical detail worthy of C.S. Forester or Patrick O'Brian will keep readers turning the pages. (Dec.)
Copyright © Reed Business Information, a division of Reed Elsevier Inc. All rights reserved.

From

The sequel to The Mark of Ran (2005) finds heroic young Rol Cortishane grown to be a much-feared sea captain. Deciding to ignore his mysterious past, he spends his energy on ship and crew. He is still an outlaw, however, and the only port he can call home is Ganesh Ka, the endangered city of exiles. When word comes from Rowan, his half-sister, asking him to fight on her behalf, he must weigh the safety of Ganesh Ka against Rowan's treachery in the past. Finally persuaded to aid Rowan, he learns more of betrayal and his heritage in the ensuing battles than he had wanted to know. Kearney's characters are much better developed here than they were in The Mark of Ran, and since the book tells a single story, the plot is tighter. Moreover, because almost all the action transpires in the here and now, the sequel can be read without reference to the predecessor. Since it ends hanging on a particularly bloody cliff, expect to see more of Kearney's excellent maritime fantasy. Frieda Murray
Copyright © American Library Association. All rights reserved

+
+*/ + From f924c45de50196e690f35d3a8301fc3a17051ae0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 20:28:59 -0600 Subject: [PATCH 51/77] Implement preferences for Book Details --- src/calibre/gui2/book_details.py | 5 +- src/calibre/gui2/library/views.py | 5 + src/calibre/gui2/preferences/look_feel.py | 106 +++++++++++++++++++++- src/calibre/gui2/preferences/look_feel.ui | 28 ++++-- 4 files changed, 129 insertions(+), 15 deletions(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index f2f048a5d5..a2ee6d343b 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -67,8 +67,9 @@ def render_html(mi, css, vertical, widget, all_fields=False): # {{{ % (table, right_pane)) return ans -def get_field_list(fm): - fieldlist = list(gprefs['book_display_fields']) +def get_field_list(fm, use_defaults=False): + src = gprefs.defaults if use_defaults else gprefs + fieldlist = list(src['book_display_fields']) names = frozenset([x[0] for x in fieldlist]) for field in fm.displayable_field_keys(): if field not in names: diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index ce5e0d9877..921e62d4c3 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -650,6 +650,11 @@ class BooksView(QTableView): # {{{ def column_map(self): return self._model.column_map + def refresh_book_details(self): + idx = self.currentIndex() + if idx.isValid(): + self._model.current_changed(idx, idx) + def scrollContentsBy(self, dx, dy): # Needed as Qt bug causes headerview to not always update when scrolling QTableView.scrollContentsBy(self, dx, dy) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index ed4312ad86..bae08f5455 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -6,7 +6,7 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, - QAbstractListModel) + QAbstractListModel, Qt) from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences.look_feel_ui import Ui_Form @@ -15,12 +15,82 @@ from calibre.utils.localization import (available_translations, get_language, get_lang) from calibre.utils.config import prefs from calibre.utils.icu import sort_key +from calibre.gui2 import NONE +from calibre.gui2.book_details import get_field_list -class DisplayedFields(QAbstractListModel): +class DisplayedFields(QAbstractListModel): # {{{ - def __init__(self, parent=None): + def __init__(self, db, parent=None): QAbstractListModel.__init__(self, parent) + self.fields = [] + self.db = db + self.changed = False + + def initialize(self, use_defaults=False): + self.fields = [[x[0], x[1]] for x in + get_field_list(self.db.field_metadata, + use_defaults=use_defaults)] + self.reset() + self.changed = True + + def rowCount(self, *args): + return len(self.fields) + + def data(self, index, role): + try: + field, visible = self.fields[index.row()] + except: + return NONE + if role == Qt.DisplayRole: + name = field + try: + name = self.db.field_metadata[field]['name'] + except: + pass + if not name: + name = field + return name + if role == Qt.CheckStateRole: + return Qt.Checked if visible else Qt.Unchecked + return NONE + + def flags(self, index): + ans = QAbstractListModel.flags(self, index) + return ans | Qt.ItemIsUserCheckable + + def setData(self, index, val, role): + ret = False + if role == Qt.CheckStateRole: + val, ok = val.toInt() + if ok: + self.fields[index.row()][1] = bool(val) + self.changed = True + ret = True + self.dataChanged.emit(index, index) + return ret + + def restore_defaults(self): + self.initialize(use_defaults=True) + + def commit(self): + if self.changed: + gprefs['book_display_fields'] = self.fields + + def move(self, idx, delta): + row = idx.row() + delta + if row >= 0 and row < len(self.fields): + t = self.fields[row] + self.fields[row] = self.fields[row-delta] + self.fields[row-delta] = t + self.dataChanged.emit(idx, idx) + idx = self.index(row) + self.dataChanged.emit(idx, idx) + self.changed = True + return idx + +# }}} + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): @@ -82,11 +152,18 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.current_font = self.initial_font = None self.change_font_button.clicked.connect(self.change_font) + self.display_model = DisplayedFields(self.gui.current_db, + self.field_display_order) + self.display_model.dataChanged.connect(self.changed_signal) + self.field_display_order.setModel(self.display_model) + self.df_up_button.clicked.connect(self.move_df_up) + self.df_down_button.clicked.connect(self.move_df_down) def initialize(self): ConfigWidgetBase.initialize(self) self.current_font = self.initial_font = gprefs['font'] self.update_font_display() + self.display_model.initialize() def restore_defaults(self): ConfigWidgetBase.restore_defaults(self) @@ -95,6 +172,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): if ofont is not None: self.changed_signal.emit() self.update_font_display() + self.display_model.restore_defaults() + self.changed_signal.emit() def build_font_obj(self): font_info = self.current_font @@ -113,6 +192,24 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.font_display.setText(name + ' [%dpt]'%fi.pointSize()) + def move_df_up(self): + idx = self.field_display_order.currentIndex() + if idx.isValid(): + idx = self.display_model.move(idx, -1) + if idx is not None: + sm = self.field_display_order.selectionModel() + sm.select(idx, sm.ClearAndSelect) + self.field_display_order.setCurrentIndex(idx) + + def move_df_down(self): + idx = self.field_display_order.currentIndex() + if idx.isValid(): + idx = self.display_model.move(idx, 1) + if idx is not None: + sm = self.field_display_order.selectionModel() + sm.select(idx, sm.ClearAndSelect) + self.field_display_order.setCurrentIndex(idx) + def change_font(self, *args): fd = QFontDialog(self.build_font_obj(), self) if fd.exec_() == fd.Accepted: @@ -129,12 +226,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): gprefs['font'] = self.current_font QApplication.setFont(self.font_display.font()) rr = True + self.display_model.commit() return rr - def refresh_gui(self, gui): self.update_font_display() gui.tags_view.reread_collapse_parameters() + gui.library_view.refresh_book_details() if __name__ == '__main__': app = QApplication([]) diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 2d5409271c..076fad2a0b 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -21,8 +21,8 @@ 0 0 - 682 - 254 + 699 + 306 @@ -189,8 +189,8 @@ 0 0 - 699 - 151 + 649 + 96 @@ -308,8 +308,8 @@ then the tags will be displayed each on their own line. 0 0 - 699 - 306 + 429 + 63 @@ -374,7 +374,7 @@ then the tags will be displayed each on their own line.
- + Select displayed metadata @@ -388,7 +388,7 @@ then the tags will be displayed each on their own line. - + Move up @@ -399,7 +399,7 @@ then the tags will be displayed each on their own line. - + Move down @@ -425,6 +425,16 @@ then the tags will be displayed each on their own line. + + + + Note that comments will always be displayed at the end, regardless of the position you assign here. + + + true + + + From 4712d19c133b3c09d3be552a7a2c40bb7bb01978 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Apr 2011 20:33:17 -0600 Subject: [PATCH 52/77] ... --- src/calibre/gui2/preferences/look_feel.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 076fad2a0b..3d8a818e1e 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -428,7 +428,7 @@ then the tags will be displayed each on their own line. - Note that comments will always be displayed at the end, regardless of the position you assign here. + Note that <b>comments</b> will always be displayed at the end, regardless of the position you assign here. true From 3941dbac1350e7b01c1ed204b9c638d8f02b5d10 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 25 Apr 2011 11:10:14 +0100 Subject: [PATCH 53/77] Add a popup options menu to the save-a-search button. --- src/calibre/gui2/layout.py | 1 - src/calibre/gui2/search_box.py | 41 ++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 7250103615..b5cc0163ed 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -214,7 +214,6 @@ class SearchBar(QWidget): # {{{ x.setIcon(QIcon(I("search_add_saved.png"))) x.setObjectName("save_search_button") l.addWidget(x) - x.setToolTip(_("Save current search under the name shown in the box")) # }}} diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index c349d84a68..19cfb7417e 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -7,12 +7,15 @@ __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' import re +from functools import partial + from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, QDialog, \ pyqtSignal, QCompleter, QAction, QKeySequence, QTimer, \ - QString, QIcon + QString, QIcon, QMenu -from calibre.gui2 import config +from calibre.gui2 import config, error_dialog +from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.search import SearchDialog from calibre.utils.search_query_parser import saved_searches @@ -330,6 +333,24 @@ class SavedSearchBox(QComboBox): # {{{ self.saved_search_selected (name) self.changed.emit() + def delete_current_search(self): + idx = self.currentIndex() + if idx <= 0: + error_dialog(self, _('Delete current search'), + _('No search is selected'), show=True) + return + if not confirm('

'+_('The selected search will be ' + 'permanently deleted. Are you sure?') + +'

', 'saved_search_delete', self): + return + ss = saved_searches().lookup(unicode(self.currentText())) + if ss is None: + return + saved_searches().delete(unicode(self.currentText())) + self.clear() + self.search_box.clear() + self.changed.emit() + # SIGNALed from the main UI def copy_search_button_clicked (self): idx = self.currentIndex(); @@ -428,6 +449,22 @@ class SavedSearchBoxMixin(object): # {{{ for x in ('copy', 'save'): b = getattr(self, x+'_search_button') b.setStatusTip(b.toolTip()) + self.save_search_button.setToolTip('

' + + _("Save current search under the name shown in the box. " + "Press and hold for a pop-up options menu.") + '

') + self.save_search_button.setMenu(QMenu()) + self.save_search_button.menu().addAction( + QIcon(I('plus.png')), + _('Create saved search'), + self.saved_search.save_search_button_clicked) + self.save_search_button.menu().addAction( + QIcon(I('trash.png')), + _('Delete saved search'), + self.saved_search.delete_current_search) + self.save_search_button.menu().addAction( + QIcon(I('search.png')), + _('Manage saved searches'), + partial(self.do_saved_search_edit, None)) def saved_searches_changed(self, set_restriction=None, recount=True): p = sorted(saved_searches().names(), key=sort_key) From c7f16d75f90e395865945e358811d7e2ac867702 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 08:41:24 -0600 Subject: [PATCH 54/77] ... --- src/calibre/ebooks/metadata/book/base.py | 2 +- src/calibre/gui2/book_details.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 5f1841d518..6c360bad96 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -631,7 +631,7 @@ class Metadata(object): res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) elif datatype == 'rating': res = res/2.0 - elif key in ('book_size', 'size'): + elif key == 'size': res = human_readable(res) return (name, unicode(res), orig_res, fmeta) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index a2ee6d343b..2eefe3cb54 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -121,10 +121,7 @@ def render_data(mi, use_roman_numbers=True, all_fields=False): continue val = prepare_string_for_xml(val) if metadata['datatype'] == 'series': - if metadata['is_custom']: - sidx = mi.get_extra(field) - else: - sidx = getattr(mi, field+'_index') + sidx = getattr(mi, field+'_index') if sidx is None: sidx = 1.0 val = _('Book %s of %s')%(fmt_sidx(sidx, From c8aedc1f174e24ecabd01540d7c3ab354e57dc54 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 09:00:22 -0600 Subject: [PATCH 55/77] Fix QToolBox on windows --- src/calibre/gui2/preferences/look_feel.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index bae08f5455..0383009381 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -11,6 +11,7 @@ from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2 import config, gprefs, qt_app +from calibre.constants import iswindows from calibre.utils.localization import (available_translations, get_language, get_lang) from calibre.utils.config import prefs @@ -158,6 +159,20 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.field_display_order.setModel(self.display_model) self.df_up_button.clicked.connect(self.move_df_up) self.df_down_button.clicked.connect(self.move_df_down) + if iswindows: + self.toolBox.setStyleSheet( + ''' + QToolBox::tab { + background: none; + border: none; + border-bottom: 2px solid black; + font-weight: bold; + } + + QToolBox::tab:selected { /* italicize selected tabs */ + font-style: italic; + } + ''') def initialize(self): ConfigWidgetBase.initialize(self) @@ -235,6 +250,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): gui.library_view.refresh_book_details() if __name__ == '__main__': - app = QApplication([]) + from calibre.gui2 import Application + app = Application([]) test_widget('Interface', 'Look & Feel') From 0dadc113d9134410626071b1a167c8ab4da1591b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 09:09:51 -0600 Subject: [PATCH 56/77] ... --- src/calibre/gui2/__init__.py | 6 ------ src/calibre/gui2/viewer/main.ui | 22 ++++++++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index f1357728ec..60d2a0a7dd 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -739,12 +739,6 @@ def build_forms(srcdir, info=None): dat = dat.replace('from QtWebKit.QWebView import QWebView', 'from PyQt4 import QtWebKit\nfrom PyQt4.QtWebKit import QWebView') - if form.endswith('viewer%smain.ui'%os.sep): - info('\t\tPromoting WebView') - dat = dat.replace('self.view = QtWebKit.QWebView(', 'self.view = DocumentView(') - dat = dat.replace('self.view = QWebView(', 'self.view = DocumentView(') - dat += '\n\nfrom calibre.gui2.viewer.documentview import DocumentView' - open(compiled_form, 'wb').write(dat) _df = os.environ.get('CALIBRE_DEVELOP_FROM', None) diff --git a/src/calibre/gui2/viewer/main.ui b/src/calibre/gui2/viewer/main.ui index 04166fe2cf..3137ad2e07 100644 --- a/src/calibre/gui2/viewer/main.ui +++ b/src/calibre/gui2/viewer/main.ui @@ -33,24 +33,21 @@ QFrame::Raised
- - - - + Qt::Vertical - + Qt::Horizontal - + QFrame::StyledPanel @@ -91,6 +88,9 @@
+ + + @@ -108,7 +108,7 @@ - Qt::LeftToolBarArea + LeftToolBarArea false @@ -136,7 +136,7 @@ - Qt::TopToolBarArea + TopToolBarArea false @@ -316,6 +316,12 @@ QWidget
QtWebKit/QWebView
+ + DocumentView + QWidget +
calibre/gui2/viewer/documentview.h
+ 1 +
From 10dd2f00d4cb65a4e8430d76d0b1b84269e755a5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 09:55:49 -0600 Subject: [PATCH 57/77] Fix #770306 (Next button does not update html source) --- src/calibre/gui2/metadata/basic_widgets.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index d34be6c564..f7872b94b9 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -18,11 +18,11 @@ from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox from calibre.utils.icu import sort_key from calibre.utils.config import tweaks, prefs -from calibre.ebooks.metadata import title_sort, authors_to_string, \ - string_to_authors, check_isbn +from calibre.ebooks.metadata import (title_sort, authors_to_string, + string_to_authors, check_isbn) from calibre.ebooks.metadata.meta import get_metadata -from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \ - choose_files, error_dialog, choose_images, question_dialog +from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, + choose_files, error_dialog, choose_images, question_dialog) from calibre.utils.date import local_tz, qt_to_dt from calibre import strftime from calibre.ebooks import BOOK_EXTENSIONS @@ -805,6 +805,7 @@ class CommentsEdit(Editor): # {{{ else: val = comments_to_html(val) self.html = val + self.wyswyg_dirtied() return property(fget=fget, fset=fset) def initialize(self, db, id_): From 2421195416d75e6ff8e4a60bf91f96720fb1045a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 10:14:22 -0600 Subject: [PATCH 58/77] Windows strikes again --- src/calibre/gui2/preferences/look_feel.ui | 402 ++++++++++------------ 1 file changed, 178 insertions(+), 224 deletions(-) diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 3d8a818e1e..4f0b8251b0 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -7,28 +7,23 @@ 0 0 717 - 444 + 390 Form - - - - - - 0 - 0 - 699 - 306 - - - - Main interface + + + + 0 + + + + Main Interface - + @@ -75,6 +70,13 @@ + + + + Enable system &tray icon (needs restart) + + + @@ -85,6 +87,13 @@ + + + + Disable &notifications in system tray + + + @@ -97,12 +106,12 @@ &Toolbar - + - + &Icon size: @@ -115,7 +124,7 @@ - + Show &text under icons: @@ -127,20 +136,7 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - + @@ -161,135 +157,15 @@ - + Change &font (needs restart) - - - - Enable system &tray icon (needs restart) - - - - - - - Disable &notifications in system tray - - - - - - - - - 0 - 0 - 649 - 96 - - - - Tag Browser - - - - - - Show &average ratings in the tags browser - - - true - - - - - - - - - Tags browser category &partitioning method: - - - opt_tags_browser_partition_method - - - - - - - Choose how tag browser subcategories are displayed when -there are more items than the limit. Select by first -letter to see an A, B, C list. Choose partitioned to -have a list of fixed-sized groups. Set to disabled -if you never want subcategories - - - - - - - &Collapse when more items than: - - - opt_tags_browser_collapse_at - - - - - - - If a Tag Browser category has more than this number of items, it is divided -up into sub-categories. If the partition method is set to disable, this value is ignored. - - - 10000 - - - - - - - Qt::Horizontal - - - - 20 - 5 - - - - - - - - - - Categories with &hierarchical items: - - - opt_categories_using_hierarchy - - - - - - - A comma-separated list of columns in which items containing -periods are displayed in the tag browser trees. For example, if -this box contains 'tags' then tags of the form 'Mystery.English' -and 'Mystery.Thriller' will be displayed with English and Thriller -both under 'Mystery'. If 'tags' is not in this box, -then the tags will be displayed each on their own line. - - - - - + + Qt::Vertical @@ -303,77 +179,11 @@ then the tags will be displayed each on their own line. - - - - 0 - 0 - 429 - 63 - - - - Cover Browser - - - - - - Show cover &browser in a separate window (needs restart) - - - - - - - &Number of covers to show in browse mode (needs restart): - - - opt_cover_flow_queue_length - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - 0 - 0 - 699 - 306 - - - + + Book Details - - - - - Use &Roman numerals for series - - - true - - - + @@ -425,6 +235,16 @@ then the tags will be displayed each on their own line. + + + + Use &Roman numerals for series + + + true + + + @@ -437,6 +257,140 @@ then the tags will be displayed each on their own line.
+ + + Tag Browser + + + + + + Tags browser category &partitioning method: + + + opt_tags_browser_partition_method + + + + + + + Choose how tag browser subcategories are displayed when +there are more items than the limit. Select by first +letter to see an A, B, C list. Choose partitioned to +have a list of fixed-sized groups. Set to disabled +if you never want subcategories + + + + + + + &Collapse when more items than: + + + opt_tags_browser_collapse_at + + + + + + + If a Tag Browser category has more than this number of items, it is divided +up into sub-categories. If the partition method is set to disable, this value is ignored. + + + 10000 + + + + + + + Show &average ratings in the tags browser + + + true + + + + + + + Categories with &hierarchical items: + + + opt_categories_using_hierarchy + + + + + + + Qt::Vertical + + + + 690 + 252 + + + + + + + + A comma-separated list of columns in which items containing +periods are displayed in the tag browser trees. For example, if +this box contains 'tags' then tags of the form 'Mystery.English' +and 'Mystery.Thriller' will be displayed with English and Thriller +both under 'Mystery'. If 'tags' is not in this box, +then the tags will be displayed each on their own line. + + + + + + + + Cover Browser + + + + + + Show cover &browser in a separate window (needs restart) + + + + + + + &Number of covers to show in browse mode (needs restart): + + + opt_cover_flow_queue_length + + + + + + + + + + Qt::Vertical + + + + 690 + 283 + + + + + +
From 3995b9386dd41d444f0aa3bef6441c00c1feff7e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 10:16:21 -0600 Subject: [PATCH 59/77] ... --- src/calibre/gui2/preferences/look_feel.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 0383009381..620113cc3f 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -11,7 +11,6 @@ from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2 import config, gprefs, qt_app -from calibre.constants import iswindows from calibre.utils.localization import (available_translations, get_language, get_lang) from calibre.utils.config import prefs @@ -159,20 +158,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.field_display_order.setModel(self.display_model) self.df_up_button.clicked.connect(self.move_df_up) self.df_down_button.clicked.connect(self.move_df_down) - if iswindows: - self.toolBox.setStyleSheet( - ''' - QToolBox::tab { - background: none; - border: none; - border-bottom: 2px solid black; - font-weight: bold; - } - - QToolBox::tab:selected { /* italicize selected tabs */ - font-style: italic; - } - ''') def initialize(self): ConfigWidgetBase.initialize(self) From ebce952f7f0d980fc3844d873bb077240fbef047 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 10:40:22 -0600 Subject: [PATCH 60/77] ... --- src/calibre/gui2/preferences/look_feel.ui | 16 ++++++++++++++++ src/calibre/gui2/preferences/main.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 4f0b8251b0..244b811cbd 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -20,6 +20,10 @@ 0 + + + :/images/lt.png:/images/lt.png + Main Interface @@ -180,6 +184,10 @@ + + + :/images/book.png:/images/book.png + Book Details @@ -258,6 +266,10 @@ + + + :/images/tags.png:/images/tags.png + Tag Browser @@ -352,6 +364,10 @@ then the tags will be displayed each on their own line. + + + :/images/cover_flow.png:/images/cover_flow.png + Cover Browser diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py index e760aa018a..c5f9a11d16 100644 --- a/src/calibre/gui2/preferences/main.py +++ b/src/calibre/gui2/preferences/main.py @@ -89,7 +89,7 @@ class Category(QWidget): # {{{ self.bar = QToolBar(self) self.bar.setStyleSheet( 'QToolBar { border: none; background: none }') - self.bar.setIconSize(QSize(48, 48)) + self.bar.setIconSize(QSize(32, 32)) self.bar.setMovable(False) self.bar.setFloatable(False) self.bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) From d7a124178c4ac36bcc61e4e40234fc025919744c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 10:49:07 -0600 Subject: [PATCH 61/77] ... --- src/calibre/gui2/book_details.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 2eefe3cb54..f1fd2c1575 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -23,7 +23,6 @@ from calibre.gui2 import (config, open_local_file, open_url, pixmap_to_data, gprefs) from calibre.utils.icu import sort_key - def render_html(mi, css, vertical, widget, all_fields=False): # {{{ table = render_data(mi, all_fields=all_fields, use_roman_numbers=config['use_roman_numerals_for_series_number']) @@ -121,7 +120,7 @@ def render_data(mi, use_roman_numbers=True, all_fields=False): continue val = prepare_string_for_xml(val) if metadata['datatype'] == 'series': - sidx = getattr(mi, field+'_index') + sidx = getattr(mi, field+'_index', None) if sidx is None: sidx = 1.0 val = _('Book %s of %s')%(fmt_sidx(sidx, From 1243bc0c2dd59bc101e0e0b50f9f22ddac3c343f Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 25 Apr 2011 17:54:17 +0100 Subject: [PATCH 62/77] Make getattr(mi, '#series_index') work, in addition to mi.get( '#series_index') which already worked. --- src/calibre/ebooks/metadata/book/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 6c360bad96..ae75072761 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -120,7 +120,11 @@ class Metadata(object): _('TEMPLATE ERROR'), self).strip() return val - + if field.startswith('#') and field.endswith('_index'): + try: + return self.get_extra(field[:-6]) + except: + pass raise AttributeError( 'Metadata object has no attribute named: '+ repr(field)) From 90326b1baaba9e8b43c7131e006982fa19c0ad91 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 11:01:21 -0600 Subject: [PATCH 63/77] ... --- src/calibre/gui2/book_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index f1fd2c1575..6b15df9a76 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -120,7 +120,7 @@ def render_data(mi, use_roman_numbers=True, all_fields=False): continue val = prepare_string_for_xml(val) if metadata['datatype'] == 'series': - sidx = getattr(mi, field+'_index', None) + sidx = mi.get(field+'_index') if sidx is None: sidx = 1.0 val = _('Book %s of %s')%(fmt_sidx(sidx, From da057256123814a4c3b622a9fbfcbc2c6945ce43 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 11:12:47 -0600 Subject: [PATCH 64/77] Small change to the API for medata source plugins. get_book_url must now return a tuple --- src/calibre/ebooks/metadata/sources/amazon.py | 2 +- src/calibre/ebooks/metadata/sources/base.py | 6 +++++- src/calibre/ebooks/metadata/sources/google.py | 2 +- src/calibre/ebooks/metadata/sources/identify.py | 7 +++---- src/calibre/gui2/metadata/single_download.py | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index 24df68e51d..8483698e28 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -301,7 +301,7 @@ class Amazon(Source): if asin is None: asin = identifiers.get('asin', None) if asin: - return 'http://amzn.com/%s'%asin + return ('amazon', asin, 'http://amzn.com/%s'%asin) # }}} def create_query(self, log, title=None, authors=None, identifiers={}): # {{{ diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index eb0277bd3f..f386489fcd 100644 --- a/src/calibre/ebooks/metadata/sources/base.py +++ b/src/calibre/ebooks/metadata/sources/base.py @@ -374,7 +374,11 @@ class Source(Plugin): def get_book_url(self, identifiers): ''' - Return the URL for the book identified by identifiers at this source. + Return a 3-tuple or None. The 3-tuple is of the form: + (identifier_type, identifier_value, URL). + The URL is the URL for the book identified by identifiers at this + source. identifier_type, identifier_value specify the identifier + corresponding to the URL. This URL must be browseable to by a human using a browser. It is meant to provide a clickable link for the user to easily visit the books page at this source. diff --git a/src/calibre/ebooks/metadata/sources/google.py b/src/calibre/ebooks/metadata/sources/google.py index 4133d4d527..b479368bac 100644 --- a/src/calibre/ebooks/metadata/sources/google.py +++ b/src/calibre/ebooks/metadata/sources/google.py @@ -173,7 +173,7 @@ class GoogleBooks(Source): def get_book_url(self, identifiers): # {{{ goog = identifiers.get('google', None) if goog is not None: - return 'http://books.google.com/books?id=%s'%goog + return ('google', goog, 'http://books.google.com/books?id=%s'%goog) # }}} def create_query(self, log, title=None, authors=None, identifiers={}): # {{{ diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 8771274f92..335d741fd2 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -438,14 +438,13 @@ def urls_from_identifiers(identifiers): # {{{ ans = [] for plugin in all_metadata_plugins(): try: - url = plugin.get_book_url(identifiers) - if url is not None: - ans.append((plugin.name, url)) + id_type, id_val, url = plugin.get_book_url(identifiers) + ans.append((plugin.name, id_type, id_val, url)) except: pass isbn = identifiers.get('isbn', None) if isbn: - ans.append((isbn, + ans.append((isbn, 'isbn', isbn, 'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn)) return ans # }}} diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 06ea8cf76a..cc89ef2259 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -253,7 +253,7 @@ class ResultsView(QTableView): # {{{ parts.append('') if book.identifiers: urls = urls_from_identifiers(book.identifiers) - ids = ['%s'%(url, name) for name, url in urls] + ids = ['%s'%(url, name) for name, ign, ign, url in urls] if ids: parts.append('
%s: %s

'%(_('See at'), ', '.join(ids))) if book.tags: From 262d1a9e931c120b290dca8195ebf7fd57abda0e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 11:33:26 -0600 Subject: [PATCH 65/77] Show identifiers in books details --- src/calibre/gui2/book_details.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 6b15df9a76..80d3c1636e 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -17,6 +17,7 @@ from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files, from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.metadata.book.base import (field_metadata, Metadata) from calibre.ebooks.metadata import fmt_sidx +from calibre.ebooks.metadata.sources.identify import urls_from_identifiers from calibre.constants import filesystem_encoding from calibre.library.comments import comments_to_html from calibre.gui2 import (config, open_local_file, open_url, pixmap_to_data, @@ -113,7 +114,12 @@ def render_data(mi, use_roman_numbers=True, all_fields=False): ans.append((field, u'%s%s'%(name, u', '.join(fmts)))) elif field == 'identifiers': - pass # TODO + urls = urls_from_identifiers(mi.identifiers) + links = [u'%s' % (url, id_typ, id_val, name) + for name, id_typ, id_val, url in urls] + links = u', '.join(links) + ans.append((field, u'%s%s'%( + _('Ids')+':', links))) else: val = mi.format_field(field)[-1] if val is None: @@ -288,6 +294,8 @@ class BookInfo(QWebView): def link_activated(self, link): self._link_clicked = True + if unicode(link.scheme()) in ('http', 'https'): + return open_url(link) link = unicode(link.toString()) self.link_clicked.emit(link) From e1c950b7bb883ae6ab00c84ff7cecc49b8b0524a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 11:42:56 -0600 Subject: [PATCH 66/77] Add support for DOI and arXiv identifiers --- src/calibre/ebooks/metadata/sources/identify.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 335d741fd2..fd71e650a0 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -435,6 +435,7 @@ def identify(log, abort, # {{{ # }}} def urls_from_identifiers(identifiers): # {{{ + identifiers = dict([(k.lower(), v) for k, v in identifiers.iteritems()]) ans = [] for plugin in all_metadata_plugins(): try: @@ -446,6 +447,14 @@ def urls_from_identifiers(identifiers): # {{{ if isbn: ans.append((isbn, 'isbn', isbn, 'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn)) + doi = identifiers.get('doi', None) + if doi: + ans.append(('DOI', 'doi', doi, + 'http://dx.doi.org/'+doi)) + arxiv = identifiers.get('arxiv', None) + if arxiv: + ans.append(('arXiv', 'arxiv', arxiv, + 'http://arxiv.org/abs/'+arxiv)) return ans # }}} From 31341a16aad3c25439b3c3d076bcc01d8a51b050 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 13:54:32 -0600 Subject: [PATCH 67/77] ... --- src/calibre/ebooks/metadata/sources/overdrive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py index 759da45610..ad570a8b28 100755 --- a/src/calibre/ebooks/metadata/sources/overdrive.py +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -265,7 +265,7 @@ class OverDrive(Source): if creators: creators = creators.split(', ') # if an exact match in a preferred format occurs - if ((author and creators[0] == author[0]) or (not author and not creators)) and od_title.lower() == title.lower() and int(formatid) in [1, 50, 410, 900] and thumbimage: + if ((author and creators and creators[0] == author[0]) or (not author and not creators)) and od_title.lower() == title.lower() and int(formatid) in [1, 50, 410, 900] and thumbimage: return self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid) else: @@ -291,7 +291,7 @@ class OverDrive(Source): close_matches.insert(0, self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) else: close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) - + elif close_title_match and close_author_match and int(formatid) in [1, 50, 410, 900]: close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) From 1ec25e442bcae43a1431064e3616681e5dd36930 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 13:58:38 -0600 Subject: [PATCH 68/77] Spruce up the confirm delete dialog --- .../gui2/dialogs/confirm_delete_location.ui | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/calibre/gui2/dialogs/confirm_delete_location.ui b/src/calibre/gui2/dialogs/confirm_delete_location.ui index 9d70388627..212d96584f 100644 --- a/src/calibre/gui2/dialogs/confirm_delete_location.ui +++ b/src/calibre/gui2/dialogs/confirm_delete_location.ui @@ -22,6 +22,12 @@ + + + 0 + 0 + + :/images/dialog_warning.png @@ -46,6 +52,10 @@ Library + + + :/images/library.png:/images/library.png + @@ -53,6 +63,10 @@ Device + + + :/images/reader.png:/images/reader.png +
@@ -60,6 +74,10 @@ Library and Device + + + :/images/trash.png:/images/trash.png + From b76159f31cf2b45fd4a75c65ec8f1cd6627a422e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 15:06:06 -0600 Subject: [PATCH 69/77] CurrentDir context now ignores the case when the original working dir no longer exists --- src/calibre/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 29c69a6799..bc99947345 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -388,7 +388,11 @@ class CurrentDir(object): return self.cwd def __exit__(self, *args): - os.chdir(self.cwd) + try: + os.chdir(self.cwd) + except: + # The previous CWD no longer exists + pass class StreamReadWrapper(object): From aced39619e9dfea8c3a58fe48eaa396039f4052f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 15:47:50 -0600 Subject: [PATCH 70/77] Add OCLC identifier to URL and use the direct ISBN link instead of worldcat search of isbn->URL --- src/calibre/ebooks/metadata/sources/identify.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index fd71e650a0..9a9e5aa164 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -446,7 +446,7 @@ def urls_from_identifiers(identifiers): # {{{ isbn = identifiers.get('isbn', None) if isbn: ans.append((isbn, 'isbn', isbn, - 'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn)) + 'http://www.worldcat.org/isbn/'+isbn)) doi = identifiers.get('doi', None) if doi: ans.append(('DOI', 'doi', doi, @@ -455,6 +455,10 @@ def urls_from_identifiers(identifiers): # {{{ if arxiv: ans.append(('arXiv', 'arxiv', arxiv, 'http://arxiv.org/abs/'+arxiv)) + oclc = identifiers.get('oclc', None) + if oclc: + ans.append(('OCLC', 'oclc', oclc, + 'http://www.worldcat.org/oclc/'+oclc)) return ans # }}} From 50334bf1a5d5ce6579848f875a7a4063181ac10d Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 25 Apr 2011 21:27:52 -0400 Subject: [PATCH 71/77] Fix bug #770534: Problem converting pdf containing the string ******************/ --- src/calibre/ebooks/conversion/preprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py index 8822a39b87..885d0621e0 100644 --- a/src/calibre/ebooks/conversion/preprocess.py +++ b/src/calibre/ebooks/conversion/preprocess.py @@ -402,7 +402,7 @@ class HTMLPreProcessor(object): (re.compile(r'((?<=)\s*file:/{2,4}[A-Z].*
|file:////?[A-Z].*
(?=\s*
))', re.IGNORECASE), lambda match: ''), # Center separator lines - (re.compile(u'
\s*(?P([*#•✦=]+\s*)+)\s*
'), lambda match: '

\n

' + match.group(1) + '

'), + (re.compile(u'
\s*(?P([*#•✦=] *){3,})\s*
'), lambda match: '

\n

' + match.group('break') + '

'), # Remove page links (re.compile(r'', re.IGNORECASE), lambda match: ''), From 16a1048522bc4ec7ff28907ec1aa3d7663e8c98f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 21:00:11 -0600 Subject: [PATCH 72/77] Document patch used on Qt for the calibre windows build to enable loading of OpenSSL dlls from the Dlls subdirectory --- setup/installer/windows/notes.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index ce6ca650a4..11b5bccf79 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -69,7 +69,24 @@ nmake -f ms\ntdll.mak install Qt -------- -Extract Qt sourcecode to C:\Qt\4.x.x. Run configure and make:: +Extract Qt sourcecode to C:\Qt\4.x.x. + +Qt uses its own routine to locate and load "system libraries" including the openssl libraries needed for "Get Books". This means that we have to apply the following patch to have Qt load the openssl libraries bundled with calibre: + + +--- src/corelib/plugin/qsystemlibrary.cpp 2011-02-22 05:04:00.000000000 -0700 ++++ src/corelib/plugin/qsystemlibrary.cpp 2011-04-25 20:53:13.635247466 -0600 +@@ -110,7 +110,7 @@ HINSTANCE QSystemLibrary::load(const wch + + #if !defined(QT_BOOTSTRAPPED) + if (!onlySystemDirectory) +- searchOrder << QFileInfo(qAppFileName()).path(); ++ searchOrder << (QFileInfo(qAppFileName()).path().replace(QLatin1Char('/'), QLatin1Char('\\')) + QString::fromLatin1("\\DLLs\\")); + #endif + searchOrder << qSystemDirectory(); + + +Now, run configure and make:: configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license -nomake examples -nomake demos -nomake docs -openssl -I Q:\openssl\include -L Q:\openssl\lib && nmake From 98b2dba03ff4b2a107e7f7b048bd62e46a3be60a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 22:09:37 -0600 Subject: [PATCH 73/77] Windows build: Have the installer overwrite dlls even if their version is the same to ensure the patched QtCore4.dll is installed --- setup/installer/windows/wix-template.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup/installer/windows/wix-template.xml b/setup/installer/windows/wix-template.xml index 37dd8b25a8..b5d2f4b292 100644 --- a/setup/installer/windows/wix-template.xml +++ b/setup/installer/windows/wix-template.xml @@ -11,7 +11,10 @@ SummaryCodepage='1252' /> - + + + Date: Mon, 25 Apr 2011 22:19:09 -0600 Subject: [PATCH 74/77] Fix the example plugin upload script --- setup/upload.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup/upload.py b/setup/upload.py index 57c8e4cd54..e448bc11e4 100644 --- a/setup/upload.py +++ b/setup/upload.py @@ -347,9 +347,10 @@ class UploadUserManual(Command): # {{{ with NamedTemporaryFile(suffix='.zip') as f: os.fchmod(f.fileno(), stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH|stat.S_IWRITE) - with CurrentDir(self.d(path)): + with CurrentDir(path): with ZipFile(f, 'w') as zf: for x in os.listdir('.'): + if x.endswith('.swp'): continue zf.write(x) if os.path.isdir(x): for y in os.listdir(x): From 80878bf7cf5379d768f514b4f3a5d665d6a44812 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 25 Apr 2011 23:48:40 -0600 Subject: [PATCH 75/77] ... --- src/calibre/ebooks/metadata/sources/base.py | 3 ++- src/calibre/manual/faq.rst | 7 ++++--- src/calibre/manual/plugins.rst | 9 +++------ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index f386489fcd..e67b87efbd 100644 --- a/src/calibre/ebooks/metadata/sources/base.py +++ b/src/calibre/ebooks/metadata/sources/base.py @@ -56,7 +56,8 @@ class InternalMetadataCompareKeyGen(object): ''' Generate a sort key for comparison of the relevance of Metadata objects, - given a search query. + given a search query. This is used only to compare results from the same + metadata source, not across different sources. The sort key ensures that an ascending order sort is a sort by order of decreasing relevance. diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index c281773660..56d1832440 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -20,13 +20,14 @@ What formats does |app| support conversion to/from? |app| supports the conversion of many input formats to many output formats. It can convert every input format in the following list, to every output format. -*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB***, PML, RB, RTF, SNB, TCR, TXT, TXTZ +*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC, PDB, PML, RB, RTF, SNB, TCR, TXT, TXTZ *Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, HTMLZ, PDB, PML, RB, PDF, SNB, TCR, TXT, TXTZ -** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers +.. note :: -*** PDB is also a generic format. |app| supports eReder, Plucker, PML and zTxt PDB files. + PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers. + PDB is also a generic format. |app| supports eReder, Plucker, PML and zTxt PDB files. .. _best-source-formats: diff --git a/src/calibre/manual/plugins.rst b/src/calibre/manual/plugins.rst index 0a62218fb9..1ebb180d46 100644 --- a/src/calibre/manual/plugins.rst +++ b/src/calibre/manual/plugins.rst @@ -65,17 +65,14 @@ Catalog plugins Metadata download plugins -------------------------- -.. module:: calibre.ebooks.metadata.fetch +.. module:: calibre.ebooks.metadata.sources.base -.. autoclass:: MetadataSource +.. autoclass:: Source :show-inheritance: :members: :member-order: bysource -.. autoclass:: calibre.ebooks.metadata.covers.CoverDownload - :show-inheritance: - :members: - :member-order: bysource +.. autoclass:: InternalMetadataCompareKeyGen Conversion plugins -------------------- From e765da76f61b7afe39f74ea4b2da9a489603f6a9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 26 Apr 2011 15:06:30 +0100 Subject: [PATCH 76/77] Fix 'count-of' searches (e.g., tags:#>3). Add a small blurb to the manual. --- src/calibre/library/caches.py | 4 +--- src/calibre/manual/gui.rst | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index ca256e0350..543c6eab96 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -406,11 +406,9 @@ class ResultCache(SearchQueryParser): # {{{ if val_func is None: loc = self.field_metadata[location]['rec_index'] val_func = lambda item, loc=loc: item[loc] - dt = self.field_metadata[location]['datatype'] - q = '' - val_func = lambda item, loc=loc: item[loc] cast = adjust = lambda x: x + dt = self.field_metadata[location]['datatype'] if query == 'false': if dt == 'rating' or location == 'cover': diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst index 7b6e60c93a..a4e18c2e07 100644 --- a/src/calibre/manual/gui.rst +++ b/src/calibre/manual/gui.rst @@ -365,6 +365,8 @@ Dates and numeric fields support the relational operators ``=`` (equals), ``>`` Rating fields are considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3 or higher. +You can search for the number of items in multiple-valued fields such as tags). These searches begin with the character ``#``, then use the same syntax as numeric fields. For example, to find all books with more than 4 tags, use ``tags:#>4``. To find all books with exactly 10 tags, use ``tags:#=10``. + Series indices are searchable. For the standard series, the search name is 'series_index'. For custom series columns, use the column search name followed by _index. For example, to search the indices for a custom series column named ``#my_series``, you would use the search name ``#my_series_index``. From e3de77792edf1479cb1e1ed3ccc595a547f49a85 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Apr 2011 09:07:16 -0600 Subject: [PATCH 77/77] ... --- src/calibre/ebooks/pdb/plucker/reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index 39ceb33b13..d782e4e97c 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -16,6 +16,7 @@ from calibre import CurrentDir from calibre.ebooks.pdb.formatreader import FormatReader from calibre.ptempfile import TemporaryFile from calibre.utils.magick import Image, create_canvas +from calibre.ebooks.compression.palmdoc import decompress_doc DATATYPE_PHTML = 0 DATATYPE_PHTML_COMPRESSED = 1