From e2b1d3bc9d77d46e567829002aa74acbb7664700 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 27 Jul 2012 17:01:04 +0530 Subject: [PATCH 01/34] Fix #1026484 (External plugins can't have PluginWidget) --- src/calibre/customize/conversion.py | 54 +++++++++++++++++++++++++++++ src/calibre/gui2/convert/bulk.py | 16 +++------ src/calibre/gui2/convert/single.py | 31 ++++------------- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/calibre/customize/conversion.py b/src/calibre/customize/conversion.py index ee8656f0ca..50bceb4def 100644 --- a/src/calibre/customize/conversion.py +++ b/src/calibre/customize/conversion.py @@ -91,6 +91,37 @@ class DummyReporter(object): def __call__(self, percent, msg=''): pass +def gui_configuration_widget(name, parent, get_option_by_name, + get_option_help, db, book_id, for_output=True): + import importlib + + def widget_factory(cls): + return cls(parent, get_option_by_name, + get_option_help, db, book_id) + + if for_output: + try: + output_widget = importlib.import_module( + 'calibre.gui2.convert.'+name) + pw = output_widget.PluginWidget + pw.ICON = I('back.png') + pw.HELP = _('Options specific to the output format.') + return widget_factory(pw) + except ImportError: + pass + else: + try: + input_widget = importlib.import_module( + 'calibre.gui2.convert.'+name) + pw = input_widget.PluginWidget + pw.ICON = I('forward.png') + pw.HELP = _('Options specific to the input format.') + return widget_factory(pw) + except ImportError: + pass + return None + + class InputFormatPlugin(Plugin): ''' InputFormatPlugins are responsible for converting a document into @@ -225,6 +256,17 @@ class InputFormatPlugin(Plugin): ''' pass + def gui_configuration_widget(self, parent, get_option_by_name, + get_option_help, db, book_id): + ''' + Called to create the widget used for configuring this plugin in the + calibre GUI. The widget must be an instance of the PluginWidget class. + See the builting input plugins for examples. + ''' + name = self.name.lower().replace(' ', '_') + return gui_configuration_widget(name, parent, get_option_by_name, + get_option_help, db, book_id, for_output=False) + class OutputFormatPlugin(Plugin): ''' @@ -308,4 +350,16 @@ class OutputFormatPlugin(Plugin): ''' pass + def gui_configuration_widget(self, parent, get_option_by_name, + get_option_help, db, book_id): + ''' + Called to create the widget used for configuring this plugin in the + calibre GUI. The widget must be an instance of the PluginWidget class. + See the builtin output plugins for examples. + ''' + name = self.name.lower().replace(' ', '_') + return gui_configuration_widget(name, parent, get_option_by_name, + get_option_help, db, book_id, for_output=True) + + diff --git a/src/calibre/gui2/convert/bulk.py b/src/calibre/gui2/convert/bulk.py index 5324a83865..3a65a4617e 100644 --- a/src/calibre/gui2/convert/bulk.py +++ b/src/calibre/gui2/convert/bulk.py @@ -4,7 +4,7 @@ __license__ = 'GPL 3' __copyright__ = '2009, John Schember ' __docformat__ = 'restructuredtext en' -import shutil, importlib +import shutil from PyQt4.Qt import QString, SIGNAL @@ -86,17 +86,9 @@ class BulkConfig(Config): sd = widget_factory(StructureDetectionWidget) toc = widget_factory(TOCWidget) - output_widget = None - name = self.plumber.output_plugin.name.lower().replace(' ', '_') - try: - output_widget = importlib.import_module( - 'calibre.gui2.convert.'+name) - pw = output_widget.PluginWidget - pw.ICON = I('back.png') - pw.HELP = _('Options specific to the output format.') - output_widget = widget_factory(pw) - except ImportError: - pass + output_widget = self.plumber.output_plugin.gui_configuration_widget( + self.stack, self.plumber.get_option_by_name, + self.plumber.get_option_help, self.db) while True: c = self.stack.currentWidget() diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py index 9160c820bd..4d13ce371b 100644 --- a/src/calibre/gui2/convert/single.py +++ b/src/calibre/gui2/convert/single.py @@ -6,7 +6,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import cPickle, shutil, importlib +import cPickle, shutil from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont @@ -187,29 +187,12 @@ class Config(ResizableDialog, Ui_Dialog): toc = widget_factory(TOCWidget) debug = widget_factory(DebugWidget) - output_widget = None - name = self.plumber.output_plugin.name.lower().replace(' ', '_') - try: - output_widget = importlib.import_module( - 'calibre.gui2.convert.'+name) - pw = output_widget.PluginWidget - pw.ICON = I('back.png') - pw.HELP = _('Options specific to the output format.') - output_widget = widget_factory(pw) - except ImportError: - pass - input_widget = None - name = self.plumber.input_plugin.name.lower().replace(' ', '_') - try: - input_widget = importlib.import_module( - 'calibre.gui2.convert.'+name) - pw = input_widget.PluginWidget - pw.ICON = I('forward.png') - pw.HELP = _('Options specific to the input format.') - input_widget = widget_factory(pw) - except ImportError: - pass - + output_widget = self.plumber.output_plugin.gui_configuration_widget( + self.stack, self.plumber.get_option_by_name, + self.plumber.get_option_help, self.db, self.book_id) + input_widget = self.plumber.input_plugin.gui_configuration_widget( + self.stack, self.plumber.get_option_by_name, + self.plumber.get_option_help, self.db, self.book_id) while True: c = self.stack.currentWidget() if not c: break From c2ca92a97a5f8e605f8abfef9f773548ba1711a3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 27 Jul 2012 17:35:11 +0530 Subject: [PATCH 02/34] ... --- manual/develop.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/manual/develop.rst b/manual/develop.rst index 3a9488ccf5..d59c315951 100644 --- a/manual/develop.rst +++ b/manual/develop.rst @@ -152,14 +152,17 @@ calibre is the directory that contains the src and resources sub-directories. En The next step is to create a bash script that will set the environment variable ``CALIBRE_DEVELOP_FROM`` to the absolute path of the src directory when running calibre in debug mode. Create a plain text file:: + #!/bin/sh export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src" calibre-debug -g Save this file as ``/usr/bin/calibre-develop``, then set its permissions so that it can be executed:: + chmod +x /usr/bin/calibre-develop -Once you have done this, type:: +Once you have done this, run:: + calibre-develop You should see some diagnostic information in the Terminal window as calibre From f728c12835dacb7419d93e3a74076fd53492542b Mon Sep 17 00:00:00 2001 From: Oliver Graf Date: Fri, 27 Jul 2012 15:01:47 +0200 Subject: [PATCH 03/34] Added extra metadata parsing to ODT module. Added manual section about ODT features added and general conversion tips. --- manual/conversion.rst | 28 ++++++++++ src/calibre/ebooks/metadata/odt.py | 85 ++++++++++++++++++++++++++++-- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/manual/conversion.rst b/manual/conversion.rst index 5eaca5a469..a4ecd902cc 100644 --- a/manual/conversion.rst +++ b/manual/conversion.rst @@ -710,3 +710,31 @@ EPUB from the ZIP file are:: Note that because this file explores the potential of EPUB, most of the advanced formatting is not going to work on readers less capable than |app|'s built-in EPUB viewer. + +Convert ODT documents +~~~~~~~~~~~~~~~~~~~~~ + +|app| can directly convert ODT (OpenDocument Text) files. You should use styles to format your document and minimize the use of direct formatting. +When inserting images into your document you need to anchor them to the paragraph, images anchored to a page will all end up in the front of the conversion. + +To enable automatic detection of chapters, you need to mark them with the build-in styles called 'Heading 1', 'Heading 2', ..., 'Heading 6' ('Heading 1' equates to the HTML tag

, 'Heading 2' to

etc). When you convert in |app| you can enter which style you used into the 'Detect chapters at' box. Example: + + * If you mark Chapters with style 'Heading 2', you have to set the 'Detect chapters at' box to ``//h:h2`` + * For a nested TOC with Sections marked with 'Heading 2' and the Chapters marked with 'Heading 3' you need to enter ``//h:h2|//h:h3``. On the Convert - TOC page set the 'Level 1 TOC' box to ``//h:h2`` and the 'Level 2 TOC' box to ``//h:h3``. + +Well-known document properties (Title, Keywords, Description, Creator) are recognized and |app| will use the first image (not to small, and with good aspect-ratio) as the cover image. + +There is also an advanced property conversion mode, which is activated by setting the custom property ``opf.metadata`` ('Yes or No' type) to Yes in your ODT document (File->Properties->Custom Properties). +If this property is detected by |app|, the following custom properties are recognized (``opf.authors`` overrides document creator):: + + opf.titlesort + opf.authors + opf.authorsort + opf.publisher + opf.pubdate + opf.isbn + opf.language + +In addition to this, you can specify the picture to use as the cover by naming it ``opf.cover`` (right click, Picture->Options->Name) in the ODT. If no picture with this name is found, the 'smart' method is used. +To prevent this you can set the custom property ``opf.nocover`` ('Yes or No' type) to Yes. + diff --git a/src/calibre/ebooks/metadata/odt.py b/src/calibre/ebooks/metadata/odt.py index bf30dfd5f7..d795b997e2 100644 --- a/src/calibre/ebooks/metadata/odt.py +++ b/src/calibre/ebooks/metadata/odt.py @@ -1,5 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +# # Copyright (C) 2006 Søren Roug, European Environment Agency # # This is free software. You may redistribute it under the terms @@ -17,12 +19,19 @@ # # Contributor(s): # +from __future__ import division + import zipfile, re import xml.sax.saxutils from cStringIO import StringIO from odf.namespaces import OFFICENS, DCNS, METANS -from calibre.ebooks.metadata import MetaInformation, string_to_authors +from odf.opendocument import load as odLoad +from odf.draw import Image as odImage, Frame as odFrame + +from calibre.ebooks.metadata import MetaInformation, string_to_authors, check_isbn +from calibre.utils.magick.draw import identify_data +from calibre.utils.date import parse_date whitespace = re.compile(r'\s+') @@ -125,6 +134,10 @@ class odfmetaparser(xml.sax.saxutils.XMLGenerator): else: texttag = self._tag self.seenfields[texttag] = self.data() + # OpenOffice has the habit to capitalize custom properties, so we add a + # lowercase version for easy access + if texttag[:4].lower() == u'opf.': + self.seenfields[texttag.lower()] = self.data() if field in self.deletefields: self.output.dowrite = True @@ -141,7 +154,7 @@ class odfmetaparser(xml.sax.saxutils.XMLGenerator): def data(self): return normalize(''.join(self._data)) -def get_metadata(stream): +def get_metadata(stream, extract_cover=True): zin = zipfile.ZipFile(stream, 'r') odfs = odfmetaparser() parser = xml.sax.make_parser() @@ -162,7 +175,73 @@ def get_metadata(stream): if data.has_key('language'): mi.language = data['language'] if data.get('keywords', ''): - mi.tags = data['keywords'].split(',') + mi.tags = map(lambda x: x.strip(), data['keywords'].split(',')) + opfmeta = False # we need this later for the cover + opfnocover = False + if data.get('opf.metadata','') == 'true': + # custom metadata contains OPF information + opfmeta = True + if data.get('opf.titlesort', ''): + mi.title_sort = data['opf.titlesort'] + if data.get('opf.authors', ''): + mi.authors = string_to_authors(data['opf.authors']) + if data.get('opf.authorsort', ''): + mi.author_sort = data['opf.authorsort'] + if data.get('opf.isbn', ''): + isbn = check_isbn(data['opf.isbn']) + if isbn is not None: + mi.isbn = isbn + if data.get('opf.publisher', ''): + mi.publisher = data['opf.publisher'] + if data.get('opf.pubdate', ''): + mi.pubdate = parse_date(data['opf.pubdate'], assume_utc=True) + if data.get('opf.language', ''): + mi.languages = [ data['opf.language'] ] + opfnocover = data.get('opf.nocover', 'false') == 'true' + # search for an draw:image in a draw:frame with the name 'opf.cover' + # if opf.metadata prop is false, just use the first image that + # has a proper size (borrowed from docx) + otext = odLoad(stream) + cover_href = None + cover_data = None + # check that it's really a ODT + if otext.mimetype == u'application/vnd.oasis.opendocument.text': + for elem in otext.text.getElementsByType(odFrame): + img = elem.getElementsByType(odImage) + if len(img) > 0: # there should be only one + i_href = img[0].getAttribute('href') + try: + raw = zin.read(i_href) + except KeyError: + continue + try: + width, height, fmt = identify_data(raw) + except: + continue + else: + continue + if opfmeta and elem.getAttribute('name').lower() == u'opf.cover': + cover_href = i_href + cover_data = (fmt, raw) + break + if cover_href is None and 0.8 <= height/width <= 1.8 and height*width >= 12000: + cover_href = i_href + cover_data = (fmt, raw) + if not opfmeta: + break + + if not opfnocover and cover_href is not None: + mi.cover = cover_href + if extract_cover: + if not cover_data: + raw = zin.read(cover_href) + try: + width, height, fmt = identify_data(raw) + except: + pass + else: + cover_data = (fmt, raw) + mi.cover_data = cover_data return mi From d13539ddaae6fe19ad70cc31f80450b3bfec70b0 Mon Sep 17 00:00:00 2001 From: Oliver Graf Date: Fri, 27 Jul 2012 19:41:19 +0200 Subject: [PATCH 04/34] Merge from main branch --- src/calibre/ebooks/metadata/odt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/odt.py b/src/calibre/ebooks/metadata/odt.py index 6b19869605..35cdb103a7 100644 --- a/src/calibre/ebooks/metadata/odt.py +++ b/src/calibre/ebooks/metadata/odt.py @@ -208,7 +208,7 @@ def get_metadata(stream, extract_cover=True): read_cover(stream, zin, mi, opfmeta, opfnocover, extract_cover) except: pass # Do not let an error reading the cover prevent reading other data - + def read_cover(stream, zin, mi, opfmeta, opfnocover, extract_cover): otext = odLoad(stream) cover_href = None From aa24da048ecff41c001e4c6732c99119d306bc47 Mon Sep 17 00:00:00 2001 From: Oliver Graf Date: Fri, 27 Jul 2012 19:43:28 +0200 Subject: [PATCH 05/34] Fix for refactoring, don't read cover if opf.nocover prop is set. --- src/calibre/ebooks/metadata/odt.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/calibre/ebooks/metadata/odt.py b/src/calibre/ebooks/metadata/odt.py index 35cdb103a7..f3e3c02c55 100644 --- a/src/calibre/ebooks/metadata/odt.py +++ b/src/calibre/ebooks/metadata/odt.py @@ -201,15 +201,18 @@ def get_metadata(stream, extract_cover=True): if cl: mi.languages = [cl] opfnocover = data.get('opf.nocover', 'false') == 'true' + if not opfnocover: + try: + read_cover(stream, zin, mi, opfmeta, extract_cover) + except: + pass # Do not let an error reading the cover prevent reading other data + + return mi + +def read_cover(stream, zin, mi, opfmeta, extract_cover): # search for an draw:image in a draw:frame with the name 'opf.cover' # if opf.metadata prop is false, just use the first image that # has a proper size (borrowed from docx) - try: - read_cover(stream, zin, mi, opfmeta, opfnocover, extract_cover) - except: - pass # Do not let an error reading the cover prevent reading other data - -def read_cover(stream, zin, mi, opfmeta, opfnocover, extract_cover): otext = odLoad(stream) cover_href = None cover_data = None @@ -239,7 +242,7 @@ def read_cover(stream, zin, mi, opfmeta, opfnocover, extract_cover): if not opfmeta: break - if not opfnocover and cover_href is not None: + if cover_href is not None: mi.cover = cover_href if extract_cover: if not cover_data: @@ -252,5 +255,3 @@ def read_cover(stream, zin, mi, opfmeta, opfnocover, extract_cover): cover_data = (fmt, raw) mi.cover_data = cover_data - return mi - From ffcbae74a58313ba4ac015236d1bcca03ec4f710 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 28 Jul 2012 11:10:13 +0530 Subject: [PATCH 06/34] ... --- src/calibre/ebooks/metadata/odt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/odt.py b/src/calibre/ebooks/metadata/odt.py index f3e3c02c55..a4371a4506 100644 --- a/src/calibre/ebooks/metadata/odt.py +++ b/src/calibre/ebooks/metadata/odt.py @@ -208,7 +208,7 @@ def get_metadata(stream, extract_cover=True): pass # Do not let an error reading the cover prevent reading other data return mi - + def read_cover(stream, zin, mi, opfmeta, extract_cover): # search for an draw:image in a draw:frame with the name 'opf.cover' # if opf.metadata prop is false, just use the first image that From e6a1a395977c890c94875076a3e9ac171d9484bd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 28 Jul 2012 12:58:41 +0530 Subject: [PATCH 07/34] Do not set first image as cover when converting ODT files --- src/calibre/ebooks/odt/input.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py index 14e1ff5892..1a70335a13 100644 --- a/src/calibre/ebooks/odt/input.py +++ b/src/calibre/ebooks/odt/input.py @@ -142,7 +142,7 @@ class Extract(ODF2XHTML): from calibre.utils.zipfile import ZipFile from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.opf2 import OPFCreator - + from calibre.customize.ui import quick_metadata if not os.path.exists(odir): os.makedirs(odir) @@ -163,7 +163,10 @@ class Extract(ODF2XHTML): zf = ZipFile(stream, 'r') self.extract_pictures(zf) stream.seek(0) - mi = get_metadata(stream, 'odt') + with quick_metadata: + # We dont want the cover, as it will lead to a duplicated image + # if no external cover is specified. + mi = get_metadata(stream, 'odt') if not mi.title: mi.title = _('Unknown') if not mi.authors: From 34d8ba6597ed1ee47cc7ae427976a79fcba67b6b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 28 Jul 2012 13:10:41 +0530 Subject: [PATCH 08/34] Fix #1030234 (Can't convert: calibre, version 0.8.62) --- src/calibre/gui2/comments_editor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 1d5e914d5f..10bcbf6218 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' import re, os from lxml import html +import sip from PyQt4.Qt import (QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, @@ -42,6 +43,7 @@ class PageAction(QAction): # {{{ self.page_action.trigger() def update_state(self, *args): + if sip.isdeleted(self) or sip.isdeleted(self.page_action): return if self.isCheckable(): self.setChecked(self.page_action.isChecked()) self.setEnabled(self.page_action.isEnabled()) From 8b62ddf46012a42b6f937ae4b938ee60d01d849b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 28 Jul 2012 19:47:14 +0530 Subject: [PATCH 09/34] ... --- src/calibre/gui2/store/stores/sony_plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/store/stores/sony_plugin.py b/src/calibre/gui2/store/stores/sony_plugin.py index 7022287794..2ad344e82c 100644 --- a/src/calibre/gui2/store/stores/sony_plugin.py +++ b/src/calibre/gui2/store/stores/sony_plugin.py @@ -32,6 +32,8 @@ class SonyStore(BasicStoreConfig, StorePlugin): d.setWindowTitle(self.name) d.set_tags(self.config.get('tags', '')) d.exec_() + else: + open_url(QUrl('http://ebookstore.sony.com')) def search(self, query, max_results=10, timeout=60): url = 'http://ebookstore.sony.com/search?keyword=%s'%urllib.quote_plus( From 7efa77639a4066de93a8290ce3ba4444d59d51cc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 28 Jul 2012 21:00:48 +0530 Subject: [PATCH 10/34] Add collation_order() ICU function --- src/calibre/utils/icu.c | 40 ++++++++++++++++++++++++++++++++++++++++ src/calibre/utils/icu.py | 15 +++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/calibre/utils/icu.c b/src/calibre/utils/icu.c index c451e9cdac..dfaf2dd53e 100644 --- a/src/calibre/utils/icu.c +++ b/src/calibre/utils/icu.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -310,6 +311,41 @@ icu_Collator_startswith(icu_Collator *self, PyObject *args, PyObject *kwargs) { Py_RETURN_FALSE; } // }}} +// Collator.startswith {{{ +static PyObject * +icu_Collator_collation_order(icu_Collator *self, PyObject *args, PyObject *kwargs) { + PyObject *a_; + size_t asz; + int32_t actual_a; + UChar *a; + wchar_t *aw; + UErrorCode status = U_ZERO_ERROR; + UCollationElements *iter = NULL; + int order = 0, len = -1; + + if (!PyArg_ParseTuple(args, "U", &a_)) return NULL; + asz = PyUnicode_GetSize(a_); + + a = (UChar*)calloc(asz*4 + 2, sizeof(UChar)); + aw = (wchar_t*)calloc(asz*4 + 2, sizeof(wchar_t)); + + if (a == NULL || aw == NULL ) return PyErr_NoMemory(); + + actual_a = (int32_t)PyUnicode_AsWideChar((PyUnicodeObject*)a_, aw, asz*4+1); + if (actual_a > -1) { + u_strFromWCS(a, asz*4 + 1, &actual_a, aw, -1, &status); + iter = ucol_openElements(self->collator, a, actual_a, &status); + if (iter != NULL && U_SUCCESS(status)) { + order = ucol_next(iter, &status); + len = ucol_getOffset(iter); + ucol_closeElements(iter); iter = NULL; + } + } + + free(a); free(aw); + return Py_BuildValue("ii", order, len); +} // }}} + static PyObject* icu_Collator_clone(icu_Collator *self, PyObject *args, PyObject *kwargs); @@ -338,6 +374,10 @@ static PyMethodDef icu_Collator_methods[] = { "startswith(a, b) -> returns True iff a startswith b, following the current collation rules." }, + {"collation_order", (PyCFunction)icu_Collator_collation_order, METH_VARARGS, + "collation_order(string) -> returns (order, length) where order is an integer that gives the position of string in a list. length gives the number of characters used for order." + }, + {NULL} /* Sentinel */ }; diff --git a/src/calibre/utils/icu.py b/src/calibre/utils/icu.py index 0dab76cd30..93f4d7b1da 100644 --- a/src/calibre/utils/icu.py +++ b/src/calibre/utils/icu.py @@ -75,6 +75,7 @@ def icu_sort_key(collator, obj): except AttributeError: return secondary_collator().sort_key(obj) + def py_find(pattern, source): pos = source.find(pattern) if pos > -1: @@ -126,6 +127,12 @@ def icu_contractions(collator): _cmap[collator] = ans return ans +def icu_collation_order(collator, a): + try: + return collator.collation_order(a) + except TypeError: + return collator.collation_order(unicode(a)) + load_icu() load_collator() _icu_not_ok = _icu is None or _collator is None @@ -205,6 +212,14 @@ def primary_startswith(a, b): except AttributeError: return icu_startswith(primary_collator(), a, b) +def collation_order(a): + if _icu_not_ok: + return (ord(a[0]), 1) if a else (0, 0) + try: + return icu_collation_order(_secondary_collator, a) + except AttributeError: + return icu_collation_order(secondary_collator(), a) + ################################################################################ def test(): # {{{ From ed9be9802e22312cc2253f5ab480d2fa1381d8a7 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 28 Jul 2012 19:12:55 +0200 Subject: [PATCH 11/34] Improved first-letter identification in tag browser using the new collation_order function. Eliminate the tweak enable_multicharacters_in_tag_browser because it no longer serves any purpose. --- resources/default_tweaks.py | 10 ---- src/calibre/gui2/tag_browser/model.py | 70 ++++++--------------------- 2 files changed, 16 insertions(+), 64 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 3bba5ecca5..c3ac9038c9 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -506,16 +506,6 @@ compile_gpm_templates = True # default_tweak_format = 'remember' default_tweak_format = None -#: Enable multi-character first-letters in the tag browser -# Some languages have letters that can be represented by multiple characters. -# For example, Czech has a 'character' "ch" that sorts between "h" and "i". -# If this tweak is True, then the tag browser will take these characters into -# consideration when partitioning by first letter. -# Examples: -# enable_multicharacters_in_tag_browser = True -# enable_multicharacters_in_tag_browser = False -enable_multicharacters_in_tag_browser = True - #: Do not preselect a completion when editing authors/tags/series/etc. # This means that you can make changes and press Enter and your changes will # not be overwritten by a matching completion. However, if you wish to use the diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 0b6a681a72..fbcf4f894e 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -9,7 +9,6 @@ __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' import traceback, cPickle, copy -from itertools import repeat from PyQt4.Qt import (QAbstractItemModel, QIcon, QVariant, QFont, Qt, QMimeData, QModelIndex, pyqtSignal, QObject) @@ -17,7 +16,7 @@ from PyQt4.Qt import (QAbstractItemModel, QIcon, QVariant, QFont, Qt, from calibre.gui2 import NONE, gprefs, config, error_dialog from calibre.library.database2 import Tag from calibre.utils.config import tweaks -from calibre.utils.icu import sort_key, lower, strcmp, contractions +from calibre.utils.icu import sort_key, lower, strcmp, collation_order from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.gui2.dialogs.confirm_delete import confirm from calibre.utils.formatter import EvalFormatter @@ -258,16 +257,6 @@ class TagsModel(QAbstractItemModel): # {{{ self.hidden_categories.add(cat) db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories)) - conts = contractions() - if len(conts) == 0 or not tweaks['enable_multicharacters_in_tag_browser']: - self.do_contraction = False - else: - self.do_contraction = True - nconts = set() - for s in conts: - nconts.add(icu_upper(s)) - self.contraction_set = frozenset(nconts) - self.db = db self._run_rebuild() self.endResetModel() @@ -416,53 +405,23 @@ class TagsModel(QAbstractItemModel): # {{{ tt = key if in_uc else None if collapse_model == 'first letter': - # Build a list of 'equal' first letters by looking for - # overlapping ranges. If a range overlaps another, then the - # letters are assumed to be equivalent. ICU collating is complex - # beyond belief. This mechanism lets us determine the logical - # first character from ICU's standpoint. - chardict = {} + # Build a list of 'equal' first letters by noticing changes + # in ICU's 'ordinal' for the first letter. In this case, the + # first letter can actually be more than one letter long. + cl_list = [None] * len(data[key]) + last_ordnum = 0 for idx,tag in enumerate(data[key]): if not tag.sort: c = ' ' else: - if not self.do_contraction: - c = icu_upper(tag.sort)[0] - else: - v = icu_upper(tag.sort) - c = v[0] - for s in self.contraction_set: - if len(s) > len(c) and v.startswith(s): - c = s - if c not in chardict: - chardict[c] = [idx, idx] - else: - chardict[c][1] = idx + c = tag.sort + ordnum, ordlen = collation_order(c) + if last_ordnum != ordnum: + last_c = icu_upper(c[0:ordlen]) + last_ordnum = ordnum + cl_list[idx] = last_c + top_level_component = 'z' + data[key][0].original_name - # sort the ranges to facilitate detecting overlap - if len(chardict) == 1 and ' ' in chardict: - # The category could not be partitioned. - collapse_model = 'disable' - else: - ranges = sorted([(v[0], v[1], c) for c,v in chardict.items()]) - # Create a list of 'first letters' to use for each item in - # the category. The list is generated using the ranges. Overlaps - # are filled with the character that first occurs. - cl_list = list(repeat(None, len(data[key]))) - for t in ranges: - start = t[0] - c = t[2] - if cl_list[start] is None: - nc = c - else: - nc = cl_list[start] - for i in range(start, t[1]+1): - cl_list[i] = nc - - if len(data[key]) > 0: - top_level_component = 'z' + data[key][0].original_name - else: - top_level_component = '' last_idx = -collapse category_is_hierarchical = not ( key in ['authors', 'publisher', 'news', 'formats', 'rating'] or @@ -587,9 +546,12 @@ class TagsModel(QAbstractItemModel): # {{{ return # }}} + import time + start_time = time.time() for category in self.category_nodes: process_one_node(category, collapse_model, state_map.get(category.category_key, {})) + print(time.time() - start_time) def get_category_editor_data(self, category): for cat in self.root_item.children: From 64a3f0e0c75d0e34df5899a87f0e99c40d62737d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 28 Jul 2012 19:15:52 +0200 Subject: [PATCH 12/34] Remove print statements --- src/calibre/gui2/tag_browser/model.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index fbcf4f894e..012f441bea 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -546,12 +546,9 @@ class TagsModel(QAbstractItemModel): # {{{ return # }}} - import time - start_time = time.time() for category in self.category_nodes: process_one_node(category, collapse_model, state_map.get(category.category_key, {})) - print(time.time() - start_time) def get_category_editor_data(self, category): for cat in self.root_item.children: From 0c25a25eb4617f501843391bd79d203bbf513950 Mon Sep 17 00:00:00 2001 From: Oliver Graf Date: Sat, 28 Jul 2012 20:34:26 +0200 Subject: [PATCH 13/34] Fix to MOBI 'Unknown' problem (replacing all empty strings with the string Unknown) --- src/calibre/ebooks/mobi/utils.py | 7 +++++-- src/calibre/ebooks/mobi/writer2/serializer.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/mobi/utils.py b/src/calibre/ebooks/mobi/utils.py index ae8e583a1b..dfdb73fdff 100644 --- a/src/calibre/ebooks/mobi/utils.py +++ b/src/calibre/ebooks/mobi/utils.py @@ -302,7 +302,7 @@ def encode_tbs(val, extra, flag_size=4): ans += encint(extra[0b0001]) return ans -def utf8_text(text): +def utf8_text(text, empty=False): ''' Convert a possibly null string to utf-8 bytes, guaranteeing to return a non empty, normalized bytestring. @@ -313,7 +313,10 @@ def utf8_text(text): text = text.decode('utf-8', 'replace') text = normalize(text).encode('utf-8') else: - text = _('Unknown').encode('utf-8') + if not empty: + text = _('Unknown').encode('utf-8') + else: + text = u''.encode('utf-8') # yeah, stupid return text def align_block(raw, multiple=4, pad=b'\0'): diff --git a/src/calibre/ebooks/mobi/writer2/serializer.py b/src/calibre/ebooks/mobi/writer2/serializer.py index 5251bf934f..87b42cd601 100644 --- a/src/calibre/ebooks/mobi/writer2/serializer.py +++ b/src/calibre/ebooks/mobi/writer2/serializer.py @@ -355,7 +355,7 @@ class Serializer(object): text = text.replace(u'\u00AD', '') # Soft-hyphen if quot: text = text.replace('"', '"') - self.buf.write(utf8_text(text)) + self.buf.write(utf8_text(text, empty=True)) def fixup_links(self): ''' From 3245270e9e24e1b249a664b782ab2391b8afdde1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 29 Jul 2012 01:36:28 +0530 Subject: [PATCH 14/34] Fix inappropriate use of utf8_text() --- src/calibre/ebooks/mobi/writer2/serializer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/mobi/writer2/serializer.py b/src/calibre/ebooks/mobi/writer2/serializer.py index 5251bf934f..1e8a204ad5 100644 --- a/src/calibre/ebooks/mobi/writer2/serializer.py +++ b/src/calibre/ebooks/mobi/writer2/serializer.py @@ -11,8 +11,9 @@ import re from calibre.ebooks.oeb.base import (OEB_DOCS, XHTML, XHTML_NS, XML_NS, namespace, prefixname, urlnormalize) +from calibre.ebooks import normalize from calibre.ebooks.mobi.mobiml import MBP_NS -from calibre.ebooks.mobi.utils import is_guide_ref_start, utf8_text +from calibre.ebooks.mobi.utils import is_guide_ref_start from collections import defaultdict from urlparse import urldefrag @@ -355,7 +356,7 @@ class Serializer(object): text = text.replace(u'\u00AD', '') # Soft-hyphen if quot: text = text.replace('"', '"') - self.buf.write(utf8_text(text)) + self.buf.write(normalize(text).encode('utf-8')) def fixup_links(self): ''' From cca78313c15cb2698c8e86e71b81470d9574845b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 29 Jul 2012 09:39:52 +0530 Subject: [PATCH 15/34] Philosophy Now by Rick Shang --- recipes/phillosophy_now.recipe | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 recipes/phillosophy_now.recipe diff --git a/recipes/phillosophy_now.recipe b/recipes/phillosophy_now.recipe new file mode 100644 index 0000000000..7c12832c70 --- /dev/null +++ b/recipes/phillosophy_now.recipe @@ -0,0 +1,75 @@ +import re +from calibre.web.feeds.recipes import BasicNewsRecipe +from collections import OrderedDict + +class PhilosophyNow(BasicNewsRecipe): + + title = 'Philosophy Now' + __author__ = 'Rick Shang' + + description = '''Philosophy Now is a lively magazine for everyone + interested in ideas. It isn't afraid to tackle all the major questions of + life, the universe and everything. Published every two months, it tries to + corrupt innocent citizens by convincing them that philosophy can be + exciting, worthwhile and comprehensible, and also to provide some enjoyable + reading matter for those already ensnared by the muse, such as philosophy + students and academics.''' + language = 'en' + category = 'news' + encoding = 'UTF-8' + + keep_only_tags = [dict(attrs={'id':'fullMainColumn'})] + remove_tags = [dict(attrs={'class':'articleTools'})] + no_javascript = True + no_stylesheets = True + needs_subscription = True + + def get_browser(self): + br = BasicNewsRecipe.get_browser() + br.open('https://philosophynow.org/auth/login') + br.select_form(nr = 1) + br['username'] = self.username + br['password'] = self.password + br.submit() + return br + + def parse_index(self): + #Go to the issue + soup0 = self.index_to_soup('http://philosophynow.org/') + issue = soup0.find('div',attrs={'id':'navColumn'}) + + #Find date & cover + cover = issue.find('div', attrs={'id':'cover'}) + date = self.tag_to_string(cover.find('h3')).strip() + self.timefmt = u' [%s]'%date + img=cover.find('img',src=True)['src'] + self.cover_url = 'http://philosophynow.org' + re.sub('medium','large',img) + issuenum = re.sub('/media/images/covers/medium/issue','',img) + issuenum = re.sub('.jpg','',issuenum) + + #Go to the main body + current_issue_url = 'http://philosophynow.org/issues/' + issuenum + soup = self.index_to_soup(current_issue_url) + div = soup.find ('div', attrs={'class':'articlesColumn'}) + + feeds = OrderedDict() + + for post in div.findAll('h3'): + articles = [] + a=post.find('a',href=True) + if a is not None: + url="http://philosophynow.org" + a['href'] + title=self.tag_to_string(a).strip() + s=post.findPrevious('h4') + section_title = self.tag_to_string(s).strip() + d=post.findNext('p') + desc = self.tag_to_string(d).strip() + articles.append({'title':title, 'url':url, 'description':desc, 'date':''}) + + if articles: + if section_title not in feeds: + feeds[section_title] = [] + feeds[section_title] += articles + ans = [(key, val) for key, val in feeds.iteritems()] + return ans + From 45deedd546aba851beadf46f8eb0abb046dac2e3 Mon Sep 17 00:00:00 2001 From: Oliver Graf Date: Sun, 29 Jul 2012 08:26:29 +0200 Subject: [PATCH 16/34] Removed uneeded utf8_text, after trunk fix. --- src/calibre/ebooks/mobi/utils.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/calibre/ebooks/mobi/utils.py b/src/calibre/ebooks/mobi/utils.py index dfdb73fdff..ae8e583a1b 100644 --- a/src/calibre/ebooks/mobi/utils.py +++ b/src/calibre/ebooks/mobi/utils.py @@ -302,7 +302,7 @@ def encode_tbs(val, extra, flag_size=4): ans += encint(extra[0b0001]) return ans -def utf8_text(text, empty=False): +def utf8_text(text): ''' Convert a possibly null string to utf-8 bytes, guaranteeing to return a non empty, normalized bytestring. @@ -313,10 +313,7 @@ def utf8_text(text, empty=False): text = text.decode('utf-8', 'replace') text = normalize(text).encode('utf-8') else: - if not empty: - text = _('Unknown').encode('utf-8') - else: - text = u''.encode('utf-8') # yeah, stupid + text = _('Unknown').encode('utf-8') return text def align_block(raw, multiple=4, pad=b'\0'): From 850356c43f8d40c09d590de28f4a005c99c37043 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 29 Jul 2012 15:57:16 +0530 Subject: [PATCH 17/34] Use a transparent background for the full screen clock to handle the case of image based backgrounds in the viewer --- src/calibre/gui2/viewer/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index c6eb76c735..207af7d510 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -507,8 +507,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.clock_label.setVisible(True) self.clock_label.setText('99:99 AA') self.clock_timer.start(1000) - self.clock_label.setStyleSheet(self.clock_label_style% - tuple(self.view.document.colors())) + self.clock_label.setStyleSheet(self.clock_label_style%( + 'rgba(0, 0, 0, 0)', + self.view.document.colors()[1])) self.clock_label.resize(self.clock_label.sizeHint()) sw = QApplication.desktop().screenGeometry(self.view) self.clock_label.move(sw.width() - self.vertical_scrollbar.width() - 15 From b39647a5de251e6d2ed6933fc4b75137f64efbbd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 29 Jul 2012 15:58:29 +0530 Subject: [PATCH 18/34] ... --- src/calibre/gui2/viewer/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 207af7d510..791fda0f93 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -508,8 +508,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.clock_label.setText('99:99 AA') self.clock_timer.start(1000) self.clock_label.setStyleSheet(self.clock_label_style%( - 'rgba(0, 0, 0, 0)', - self.view.document.colors()[1])) + 'rgba(0, 0, 0, 0)', self.view.document.colors()[1])) self.clock_label.resize(self.clock_label.sizeHint()) sw = QApplication.desktop().screenGeometry(self.view) self.clock_label.move(sw.width() - self.vertical_scrollbar.width() - 15 From a380aef221b793e451dcd4af02e201705bf9c657 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 30 Jul 2012 10:10:45 +0530 Subject: [PATCH 19/34] ... --- manual/news.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual/news.rst b/manual/news.rst index 873025d467..9783a262aa 100755 --- a/manual/news.rst +++ b/manual/news.rst @@ -30,7 +30,7 @@ Lets pick a couple of feeds that look interesting: #. Business Travel: http://feeds.portfolio.com/portfolio/businesstravel #. Tech Observer: http://feeds.portfolio.com/portfolio/thetechobserver -I got the URLs by clicking the little orange RSS icon next to each feed name. To make |app| download the feeds and convert them into an ebook, you should click the :guilabel:`Fetch news` button and then the :guilabel:`Add a custom news source` menu item. A dialog similar to that shown below should open up. +I got the URLs by clicking the little orange RSS icon next to each feed name. To make |app| download the feeds and convert them into an ebook, you should right click the :guilabel:`Fetch news` button and then the :guilabel:`Add a custom news source` menu item. A dialog similar to that shown below should open up. .. image:: images/custom_news.png :align: center From 957d3b3c0997c170198d5bf237ee47daf9af7da6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 30 Jul 2012 10:54:17 +0530 Subject: [PATCH 20/34] KF8 Input: Handle html entities in the NCX toc entries correctly --- src/calibre/ebooks/mobi/reader/ncx.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/mobi/reader/ncx.py b/src/calibre/ebooks/mobi/reader/ncx.py index ca3255e100..d3747f6a8a 100644 --- a/src/calibre/ebooks/mobi/reader/ncx.py +++ b/src/calibre/ebooks/mobi/reader/ncx.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' import os +from calibre import replace_entities from calibre.ebooks.metadata.toc import TOC from calibre.ebooks.mobi.reader.headers import NULL_INDEX from calibre.ebooks.mobi.reader.index import read_index @@ -88,7 +89,8 @@ def build_toc(index_entries): for lvl in sorted(levels): for item in level_map[lvl]: parent = num_map[item['parent']] - child = parent.add_item(item['href'], item['idtag'], item['text']) + child = parent.add_item(item['href'], item['idtag'], + replace_entities(item['text'], encoding=None)) num_map[item['num']] = child # Set play orders in depth first order From c33da71191fb9cf1a2bd6d134c306782ec9286b9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 30 Jul 2012 12:34:52 +0530 Subject: [PATCH 21/34] Fix detecting of file extensions in DnD events --- src/calibre/gui2/dnd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/dnd.py b/src/calibre/gui2/dnd.py index 90b7e1e0ca..9f8824ef23 100644 --- a/src/calibre/gui2/dnd.py +++ b/src/calibre/gui2/dnd.py @@ -142,14 +142,14 @@ def dnd_has_extension(md, extensions): if md.hasUrls(): urls = [unicode(u.toString()) for u in md.urls()] - purls = [urlparse(u) for u in urls] - paths = [u2p(x) for x in purls] + paths = [urlparse(u).path for u in urls] + exts = frozenset([posixpath.splitext(u)[1][1:].lower() for u in + paths if u]) if DEBUG: prints('URLS:', urls) prints('Paths:', paths) + prints('Extensions:', exts) - exts = frozenset([posixpath.splitext(u)[1][1:].lower() for u in - paths]) return bool(exts.intersection(frozenset(extensions))) return False From c0c435712c9b14246d4bed773feacf643c0e4597 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 30 Jul 2012 13:28:30 +0530 Subject: [PATCH 22/34] ... --- src/calibre/gui2/dnd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/dnd.py b/src/calibre/gui2/dnd.py index 9f8824ef23..c474fed537 100644 --- a/src/calibre/gui2/dnd.py +++ b/src/calibre/gui2/dnd.py @@ -135,7 +135,8 @@ def dnd_has_extension(md, extensions): prints('Debugging DND event') for f in md.formats(): f = unicode(f) - prints(f, repr(data_as_string(f, md))[:300], '\n') + raw = data_as_string(f, md) + prints(f, len(raw), repr(raw[:300]), '\n') print () if has_firefox_ext(md, extensions): return True From e4ec25db991c85478bc34b6b77ff3e2f3cb48035 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 30 Jul 2012 14:25:14 +0530 Subject: [PATCH 23/34] Fix #1026421 (Private bug) --- src/calibre/ebooks/mobi/writer8/skeleton.py | 32 +++++++-------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/calibre/ebooks/mobi/writer8/skeleton.py b/src/calibre/ebooks/mobi/writer8/skeleton.py index 5db6ee0b5c..ae8fdf364c 100644 --- a/src/calibre/ebooks/mobi/writer8/skeleton.py +++ b/src/calibre/ebooks/mobi/writer8/skeleton.py @@ -76,15 +76,13 @@ def tostring(raw, **kwargs): class Chunk(object): - def __init__(self, raw, parent_tag): + def __init__(self, raw, selector): self.raw = raw self.starts_tags = [] self.ends_tags = [] self.insert_pos = None - self.parent_tag = parent_tag - self.parent_is_body = False - self.is_last_chunk = False self.is_first_chunk = False + self.selector = "%s-//*[@aid='%s']"%selector def __len__(self): return len(self.raw) @@ -97,11 +95,6 @@ class Chunk(object): return 'Chunk(len=%r insert_pos=%r starts_tags=%r ends_tags=%r)'%( len(self.raw), self.insert_pos, self.starts_tags, self.ends_tags) - @property - def selector(self): - typ = 'S' if (self.is_last_chunk and not self.parent_is_body) else 'P' - return "%s-//*[@aid='%s']"%(typ, self.parent_tag) - __str__ = __repr__ class Skeleton(object): @@ -251,13 +244,13 @@ class Chunker(object): def step_into_tag(self, tag, chunks): aid = tag.get('aid') - is_body = tag.tag == 'body' + self.chunk_selector = ('P', aid) first_chunk_idx = len(chunks) # First handle any text if tag.text and tag.text.strip(): # Leave pure whitespace in the skel - chunks.extend(self.chunk_up_text(tag.text, aid)) + chunks.extend(self.chunk_up_text(tag.text)) tag.text = None # Now loop over children @@ -266,21 +259,21 @@ class Chunker(object): if child.tag == etree.Entity: chunks.append(raw) if child.tail: - chunks.extend(self.chunk_up_text(child.tail, aid)) + chunks.extend(self.chunk_up_text(child.tail)) continue raw = close_self_closing_tags(raw) if len(raw) > CHUNK_SIZE and child.get('aid', None): self.step_into_tag(child, chunks) if child.tail and child.tail.strip(): # Leave pure whitespace - chunks.extend(self.chunk_up_text(child.tail, aid)) + chunks.extend(self.chunk_up_text(child.tail)) child.tail = None else: if len(raw) > CHUNK_SIZE: self.log.warn('Tag %s has no aid and a too large chunk' ' size. Adding anyway.'%child.tag) - chunks.append(Chunk(raw, aid)) + chunks.append(Chunk(raw, self.chunk_selector)) if child.tail: - chunks.extend(self.chunk_up_text(child.tail, aid)) + chunks.extend(self.chunk_up_text(child.tail)) tag.remove(child) if len(chunks) <= first_chunk_idx and chunks: @@ -293,12 +286,9 @@ class Chunker(object): my_chunks = chunks[first_chunk_idx:] if my_chunks: my_chunks[0].is_first_chunk = True - my_chunks[-1].is_last_chunk = True - if is_body: - for chunk in my_chunks: - chunk.parent_is_body = True + self.chunk_selector = ('S', aid) - def chunk_up_text(self, text, parent_tag): + def chunk_up_text(self, text): text = text.encode('utf-8') ans = [] @@ -314,7 +304,7 @@ class Chunker(object): while rest: start, rest = split_multibyte_text(rest) ans.append(b'' + start + '') - return [Chunk(x, parent_tag) for x in ans] + return [Chunk(x, self.chunk_selector) for x in ans] def merge_small_chunks(self, chunks): ans = chunks[:1] From b4dcafa78c63272a5552a4f0377c30eae60076b2 Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 30 Jul 2012 10:39:58 -0600 Subject: [PATCH 24/34] Handle authors whose last name is a number. Indeed. --- src/calibre/library/catalogs/epub_mobi_builder.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 7cbd639fd7..76f96a3397 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -2637,8 +2637,10 @@ Author '{0}': navLabelTag.insert(0, textTag) navPointByLetterTag.insert(0,navLabelTag) contentTag = Tag(soup, 'content') - contentTag['src'] = "%s#%s_authors" % (HTML_file, self.generateUnicodeName(authors_by_letter[1])) - + if authors_by_letter[1] == self.SYMBOLS: + contentTag['src'] = "%s#%s_authors" % (HTML_file, authors_by_letter[1]) + else: + contentTag['src'] = "%s#%s_authors" % (HTML_file, self.generateUnicodeName(authors_by_letter[1])) navPointByLetterTag.insert(1,contentTag) if self.generateForKindle: From 895b028101a0f8f84e56cf9d7687c687e08ee6d2 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 31 Jul 2012 09:40:56 +0200 Subject: [PATCH 25/34] Version of smartdevice driver for beta test --- resources/images/devices/galaxy_s3.png | Bin 0 -> 103454 bytes src/calibre/customize/builtins.py | 3 +- .../devices/smart_device_app/__init__.py | 9 + .../devices/smart_device_app/driver.py | 869 ++++++++++++++++++ 4 files changed, 880 insertions(+), 1 deletion(-) create mode 100644 resources/images/devices/galaxy_s3.png create mode 100644 src/calibre/devices/smart_device_app/__init__.py create mode 100644 src/calibre/devices/smart_device_app/driver.py diff --git a/resources/images/devices/galaxy_s3.png b/resources/images/devices/galaxy_s3.png new file mode 100644 index 0000000000000000000000000000000000000000..1aef78e20d6fd420dacc76a108f31ca0070d1936 GIT binary patch literal 103454 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 bIr)D9X-e#6T#omC00000NkvXXu0mjfQZ{ex literal 0 HcmV?d00001 diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 52cd7781e6..6f443a0013 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -673,7 +673,7 @@ from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.kobo.driver import KOBO from calibre.devices.bambook.driver import BAMBOOK from calibre.devices.boeye.driver import BOEYE_BEX, BOEYE_BDX - +from calibre.devices.smart_device_app.driver import SMART_DEVICE_APP # Order here matters. The first matched device is the one used. @@ -746,6 +746,7 @@ plugins += [ ITUNES, BOEYE_BEX, BOEYE_BDX, + SMART_DEVICE_APP, USER_DEFINED, ] # }}} diff --git a/src/calibre/devices/smart_device_app/__init__.py b/src/calibre/devices/smart_device_app/__init__.py new file mode 100644 index 0000000000..0080175bfa --- /dev/null +++ b/src/calibre/devices/smart_device_app/__init__.py @@ -0,0 +1,9 @@ +#!/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/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py new file mode 100644 index 0000000000..b3a2f90ecb --- /dev/null +++ b/src/calibre/devices/smart_device_app/driver.py @@ -0,0 +1,869 @@ +''' +Created on 29 Jun 2012 + +@author: charles +''' +import socket, select, json, inspect, os, traceback, time, sys, random +import hashlib, threading +from base64 import b64encode, b64decode +from functools import wraps + +from calibre.constants import numeric_version, DEBUG +from calibre.devices.interface import DevicePlugin +from calibre.devices.usbms.books import Book, BookList +from calibre.devices.usbms.deviceconfig import DeviceConfig +from calibre.devices.usbms.driver import USBMS +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.ebooks.metadata import title_sort +from calibre.ebooks.metadata.book import SERIALIZABLE_FIELDS +from calibre.ebooks.metadata.book.base import Metadata +from calibre.ebooks.metadata.book.json_codec import JsonCodec +from calibre.library import current_library_name +from calibre.utils.ipc import eintr_retry_call +from calibre.utils.config import from_json, tweaks +from calibre.utils.date import isoformat, now +from calibre.utils.filenames import ascii_filename as sanitize, shorten_components_to +from calibre.utils.mdns import publish as publish_zeroconf +from calibre.utils.mdns import unpublish as unpublish_zeroconf + +def synchronous( tlockname ): + """A decorator to place an instance based lock around a method """ + + def _synched(func): + @wraps(func) + def _synchronizer(self,*args, **kwargs): + tlock = self.__getattribute__( tlockname) + tlock.acquire() + try: + return func(self, *args, **kwargs) + finally: + tlock.release() + return _synchronizer + return _synched + + +class SMART_DEVICE_APP (DeviceConfig, DevicePlugin): + name = 'SmartDevice App Interface' + gui_name = _('SmartDevice') + icon = I('devices/galaxy_s3.png') + description = _('Communicate with Smart Device apps') + supported_platforms = ['windows', 'osx', 'linux'] + author = 'Charles Haley' + version = (0, 0, 1) + + # Invalid USB vendor information so the scanner will never match + VENDOR_ID = [0xffff] + PRODUCT_ID = [0xffff] + BCD = [0xffff] + + FORMATS = list(BOOK_EXTENSIONS) + ALL_FORMATS = list(BOOK_EXTENSIONS) + HIDE_FORMATS_CONFIG_BOX = True + USER_CAN_ADD_NEW_FORMATS = False + DEVICE_PLUGBOARD_NAME = 'SMART_DEVICE_APP' + CAN_SET_METADATA = [] + CAN_DO_DEVICE_DB_PLUGBOARD = False + SUPPORTS_SUB_DIRS = False + MUST_READ_METADATA = True + NEWS_IN_FOLDER = False + SUPPORTS_USE_AUTHOR_SORT = False + WANTS_UPDATED_THUMBNAILS = True + MAX_PATH_LEN = 100 + THUMBNAIL_HEIGHT = 160 + PREFIX = '' + + # Some network protocol constants + BASE_PACKET_LEN = 4096 + PROTOCOL_VERSION = 1 + MAX_CLIENT_COMM_TIMEOUT = 60.0 # Wait at most N seconds for an answer + + opcodes = { + 'NOOP' : 12, + 'OK' : 0, + 'BOOK_DATA' : 10, + 'BOOK_DONE' : 11, + 'DELETE_BOOK' : 13, + 'DISPLAY_MESSAGE' : 17, + 'FREE_SPACE' : 5, + 'GET_BOOK_FILE_SEGMENT' : 14, + 'GET_BOOK_METADATA' : 15, + 'GET_BOOK_COUNT' : 6, + 'GET_DEVICE_INFORMATION' : 3, + 'GET_INITIALIZATION_INFO': 9, + 'SEND_BOOKLISTS' : 7, + 'SEND_BOOK' : 8, + 'SEND_BOOK_METADATA' : 16, + 'SET_CALIBRE_DEVICE_INFO': 1, + 'SET_CALIBRE_DEVICE_NAME': 2, + 'TOTAL_SPACE' : 4, + } + reverse_opcodes = dict([(v, k) for k,v in opcodes.iteritems()]) + + + EXTRA_CUSTOMIZATION_MESSAGE = [ + _('Enable connections at startup') + ':::

' + + _('Check this box to allow connections when calibre starts') + '

', + '', + _('Security password') + ':::

' + + _('Enter a password that the device app must use to connect to calibre') + '

', + '', + _('Print extra debug information') + ':::

' + + _('Check this box if requested when reporting problems') + '

', + ] + EXTRA_CUSTOMIZATION_DEFAULT = [ + False, + '', + '', + '', + False, + ] + OPT_AUTOSTART = 0 + OPT_PASSWORD = 2 + OPT_EXTRA_DEBUG = 4 + + def __init__(self, path): + self.sync_lock = threading.RLock() + self.noop_counter = 0 + self.debug_start_time = time.time() + self.debug_time = time.time() + + def _debug(self, *args): + if not DEBUG: + return + total_elapsed = time.time() - self.debug_start_time + elapsed = time.time() - self.debug_time + print 'SMART_DEV (%7.2f:%7.3f) %s'%(total_elapsed, elapsed, + inspect.stack()[1][3]), + for a in args: + print a, + print + self.debug_time = time.time() + + # Various methods required by the plugin architecture + @classmethod + def _default_save_template(cls): + from calibre.library.save_to_disk import config + st = cls.SAVE_TEMPLATE if cls.SAVE_TEMPLATE else \ + config().parse().send_template + if st: + st = os.path.basename(st) + return st + + @classmethod + def save_template(cls): + st = cls.settings().save_template + if st: + st = os.path.basename(st) + else: + st = cls._default_save_template() + return st + + # local utilities + + # copied from USBMS. Perhaps this could be a classmethod in usbms? + def _update_driveinfo_record(self, dinfo, prefix, location_code, name=None): + import uuid + if not isinstance(dinfo, dict): + dinfo = {} + if dinfo.get('device_store_uuid', None) is None: + dinfo['device_store_uuid'] = unicode(uuid.uuid4()) + if dinfo.get('device_name') is None: + dinfo['device_name'] = self.get_gui_name() + if name is not None: + dinfo['device_name'] = name + dinfo['location_code'] = location_code + dinfo['last_library_uuid'] = getattr(self, 'current_library_uuid', None) + dinfo['calibre_version'] = '.'.join([unicode(i) for i in numeric_version]) + dinfo['date_last_connected'] = isoformat(now()) + dinfo['prefix'] = self.PREFIX + return dinfo + + # copied with changes from USBMS.Device. In particular, we needed to + # remove the 'path' argument and all its uses. Also removed the calls to + # filename_callback and sanitize_path_components + def _create_upload_path(self, mdata, fname, create_dirs=True): + maxlen = self.MAX_PATH_LEN + + special_tag = None + if mdata.tags: + for t in mdata.tags: + if t.startswith(_('News')) or t.startswith('/'): + special_tag = t + break + + settings = self.settings() + template = self.save_template() + if mdata.tags and _('News') in mdata.tags: + try: + p = mdata.pubdate + date = (p.year, p.month, p.day) + except: + today = time.localtime() + date = (today[0], today[1], today[2]) + template = "{title}_%d-%d-%d" % date + use_subdirs = self.SUPPORTS_SUB_DIRS and settings.use_subdirs + + fname = sanitize(fname) + ext = os.path.splitext(fname)[1] + + from calibre.library.save_to_disk import get_components + from calibre.library.save_to_disk import config + opts = config().parse() + if not isinstance(template, unicode): + template = template.decode('utf-8') + app_id = str(getattr(mdata, 'application_id', '')) + id_ = mdata.get('id', fname) + extra_components = get_components(template, mdata, id_, + timefmt=opts.send_timefmt, length=maxlen-len(app_id)-1) + if not extra_components: + extra_components.append(sanitize(fname)) + else: + extra_components[-1] = sanitize(extra_components[-1]+ext) + + if extra_components[-1] and extra_components[-1][0] in ('.', '_'): + extra_components[-1] = 'x' + extra_components[-1][1:] + + if special_tag is not None: + name = extra_components[-1] + extra_components = [] + tag = special_tag + if tag.startswith(_('News')): + if self.NEWS_IN_FOLDER: + extra_components.append('News') + else: + for c in tag.split('/'): + c = sanitize(c) + if not c: continue + extra_components.append(c) + extra_components.append(name) + + if not use_subdirs: + # Leave this stuff here in case we later decide to use subdirs + extra_components = extra_components[-1:] + + def remove_trailing_periods(x): + ans = x + while ans.endswith('.'): + ans = ans[:-1].strip() + if not ans: + ans = 'x' + return ans + + extra_components = list(map(remove_trailing_periods, extra_components)) + components = shorten_components_to(maxlen, extra_components) + filepath = os.path.join(*components) + return filepath + + def _strip_prefix(self, path): + if self.PREFIX and path.startswith(self.PREFIX): + return path[len(self.PREFIX):] + return path + + # JSON booklist encode & decode + + # If the argument is a booklist or contains a book, use the metadata json + # codec to first convert it to a string dict + def _json_encode(self, op, arg): + res = {} + for k,v in arg.iteritems(): + if isinstance(v, (Book, Metadata)): + res[k] = self.json_codec.encode_book_metadata(v) + series = v.get('series', None) + if series: + tsorder = tweaks['save_template_title_series_sorting'] + series = title_sort(v.get('series', ''), order=tsorder) + else: + series = '' + res[k]['_series_sort_'] = series + else: + res[k] = v + return json.dumps([op, res], encoding='utf-8') + + # Network functions + def _read_string_from_net(self, conn): + data = bytes(0) + while True: + dex = data.find('[') + if dex >= 0: + break + # conn.recv seems to return a pointer into some internal buffer. + # Things get trashed if we don't make a copy of the data. + self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) + v = conn.recv(self.BASE_PACKET_LEN) + self.device_socket.settimeout(None) + if len(v) == 0: + return '' # documentation says the socket is broken permanently. + data += v + total_len = int(data[:dex]) + data = data[dex:] + pos = len(data) + while pos < total_len: + self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) + v = conn.recv(total_len - pos) + self.device_socket.settimeout(None) + if len(v) == 0: + return '' # documentation says the socket is broken permanently. + data += v + pos += len(v) + return data + + def _call_client(self, op, arg, print_debug_info=True): + if op != 'NOOP': + self.noop_counter = 0 + extra_debug = self.settings().extra_customization[self.OPT_EXTRA_DEBUG] + if print_debug_info or extra_debug: + if extra_debug: + self._debug(op, arg) + else: + self._debug(op) + if self.device_socket is None: + return None, None + try: + s = self._json_encode(self.opcodes[op], arg) + if print_debug_info and extra_debug: + self._debug('send string', s) + self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) + self.device_socket.sendall(('%d' % len(s))+s) + self.device_socket.settimeout(None) + v = self._read_string_from_net(self.device_socket) + if print_debug_info and extra_debug: + self._debug('received string', v) + if v: + v = json.loads(v, object_hook=from_json) + if print_debug_info and extra_debug: + self._debug('receive after decode') #, v) + return (self.reverse_opcodes[v[0]], v[1]) + self._debug('protocol error -- empty json string') + except socket.timeout: + self._debug('timeout communicating with device') + self.device_socket.close() + self.device_socket = None + raise IOError(_('Device did not respond in reasonable time')) + except socket.error: + self._debug('device went away') + self.device_socket.close() + self.device_socket = None + raise IOError(_('Device closed the network connection')) + except: + self._debug('other exception') + traceback.print_exc() + self.device_socket.close() + self.device_socket = None + raise + raise IOError('Device responded with incorrect information') + + # Write a file as a series of base64-encoded strings. + def _put_file(self, infile, lpath, book_metadata, this_book, total_books): + close_ = False + if not hasattr(infile, 'read'): + infile, close_ = open(infile, 'rb'), True + infile.seek(0, os.SEEK_END) + length = infile.tell() + book_metadata.size = length + infile.seek(0) + self._debug(lpath, length) + self._call_client('SEND_BOOK', {'lpath': lpath, 'length': length, + 'metadata': book_metadata, 'thisBook': this_book, + 'totalBooks': total_books}, print_debug_info=False) + self._set_known_metadata(book_metadata) + pos = 0 + failed = False + with infile: + while True: + b = infile.read(self.max_book_packet_len) + blen = len(b) + if not b: + break; + b = b64encode(b) + opcode, result = self._call_client('BOOK_DATA', + {'lpath': lpath, 'position': pos, 'data': b}, + print_debug_info=False) + pos += blen + if opcode != 'OK': + self._debug('protocol error', opcode) + failed = True + break + self._call_client('BOOK_DONE', {'lpath': lpath}) + self.time = None + if close_: + infile.close() + return -1 if failed else length + + def _get_smartdevice_option_number(self, opt_string): + if opt_string == 'password': + return self.OPT_PASSWORD + elif opt_string == 'autostart': + return self.OPT_AUTOSTART + else: + return None + + def _compare_metadata(self, mi1, mi2): + for key in SERIALIZABLE_FIELDS: + if key in ['cover', 'mime']: + continue + if key == 'user_metadata': + meta1 = mi1.get_all_user_metadata(make_copy=False) + meta2 = mi1.get_all_user_metadata(make_copy=False) + if meta1 != meta2: + self._debug('custom metadata different') + return False + for ckey in meta1: + if mi1.get(ckey) != mi2.get(ckey): + self._debug(ckey, mi1.get(ckey), mi2.get(ckey)) + return False + elif mi1.get(key, None) != mi2.get(key, None): + self._debug(key, mi1.get(key), mi2.get(key)) + return False + return True + + def _metadata_already_on_device(self, book): + v = self.known_metadata.get(book.lpath, None) + if v is not None: + return self._compare_metadata(book, v) + return False + + def _set_known_metadata(self, book, remove=False): + lpath = book.lpath + if remove: + self.known_metadata[lpath] = None + else: + self.known_metadata[lpath] = book.deepcopy() + + # The public interface methods. + + + @synchronous('sync_lock') + def is_usb_connected(self, devices_on_system, debug=False, only_presence=False): + if getattr(self, 'listen_socket', None) is None: + self.is_connected = False + if self.is_connected: + self.noop_counter += 1 + if only_presence and (self.noop_counter % 5) != 1: + ans = select.select((self.device_socket,), (), (), 0) + if len(ans[0]) == 0: + return (True, self) + # The socket indicates that something is there. Given the + # protocol, this can only be a disconnect notification. Fall + # through and actually try to talk to the client. + try: + # This will usually toss an exception if the socket is gone. + try: + if self._call_client('NOOP', dict())[0] is None: + self.is_connected = False + except: + self.is_connected = False + except: + self.is_connected = False + return (self.is_connected, self) + if getattr(self, 'listen_socket', None) is not None: + ans = select.select((self.listen_socket,), (), (), 0) + if len(ans[0]) > 0: + # timeout in 10 ms to detect rare case where the socket went + # way between the select and the accent + try: + self.device_socket = None + self.listen_socket.settimeout(0.010) + self.device_socket, ign = \ + eintr_retry_call(self.listen_socket.accept) + self.listen_socket.settimeout(None) + self.device_socket.settimeout(None) + self.is_connected = True + except socket.timeout: + if self.device_socket is not None: + self.device_socket.close() + except socket.error: + x = sys.exc_info()[1] + self._debug('unexpected socket exception', x.args[0]) + if self.device_socket is not None: + self.device_socket.close() + raise + return (True, self) + return (False, None) + + @synchronous('sync_lock') + def open(self, connected_device, library_uuid): + self._debug() + self.current_library_uuid = library_uuid + self.current_library_name = current_library_name() + try: + password = self.settings().extra_customization[self.OPT_PASSWORD] + if password: + challenge = isoformat(now()) + hasher = hashlib.new('sha1') + hasher.update(password.encode('UTF-8')) + hasher.update(challenge.encode('UTF-8')) + hash_digest = hasher.hexdigest() + else: + challenge = '' + hash_digest = '' + opcode, result = self._call_client('GET_INITIALIZATION_INFO', + {'serverProtocolVersion': self.PROTOCOL_VERSION, + 'validExtensions': self.ALL_FORMATS, + 'passwordChallenge': challenge, + 'currentLibraryName': self.current_library_name, + 'currentLibraryUUID': library_uuid, + 'rightAnswer': hash_digest}) + if opcode != 'OK': + # Something wrong with the return. Close the socket + # and continue. + self._debug('Protocol error - Opcode not OK') + self.device_socket.close() + return False + if not result.get('versionOK', False): + # protocol mismatch + self._debug('Protocol error - protocol version mismatch') + self.device_socket.close() + return False + if result.get('maxBookContentPacketLen', 0) <= 0: + # protocol mismatch + self._debug('Protocol error - bogus book packet length') + self.device_socket.close() + return False + self.max_book_packet_len = result.get('maxBookContentPacketLen', + self.BASE_PACKET_LEN) + exts = result.get('acceptedExtensions', None) + if exts is None or not isinstance(exts, list) or len(exts) == 0: + self._debug('Protocol error - bogus accepted extensions') + self.device_socket.close() + return False + self.FORMATS = exts + if password: + returned_hash = result.get('passwordHash', None) + if result.get('passwordHash', None) is None: + # protocol mismatch + self._debug('Protocol error - missing password hash') + self.device_socket.close() + return False + if returned_hash != hash_digest: + # bad password + self._debug('password mismatch') + self._call_client("DISPLAY_MESSAGE", {'messageKind':1}) + self.device_socket.close() + return False + return True + except socket.timeout: + self.device_socket.close() + except socket.error: + x = sys.exc_info()[1] + self._debug('unexpected socket exception', x.args[0]) + self.device_socket.close() + raise + return False + + @synchronous('sync_lock') + def get_device_information(self, end_session=True): + self._debug() + self.report_progress(1.0, _('Get device information...')) + opcode, result = self._call_client('GET_DEVICE_INFORMATION', dict()) + if opcode == 'OK': + self.driveinfo = result['device_info'] + self._update_driveinfo_record(self.driveinfo, self.PREFIX, 'main') + self._call_client('SET_CALIBRE_DEVICE_INFO', self.driveinfo) + return (self.get_gui_name(), result['device_version'], + result['version'], '', {'main':self.driveinfo}) + return (self.get_gui_name(), '', '', '') + + @synchronous('sync_lock') + def set_driveinfo_name(self, location_code, name): + self._update_driveinfo_record(self.driveinfo, "main", name) + self._call_client('SET_CALIBRE_DEVICE_NAME', + {'location_code': 'main', 'name':name}) + + @synchronous('sync_lock') + def reset(self, key='-1', log_packets=False, report_progress=None, + detected_device=None) : + self._debug() + self.set_progress_reporter(report_progress) + + @synchronous('sync_lock') + def set_progress_reporter(self, report_progress): + self._debug() + self.report_progress = report_progress + if self.report_progress is None: + self.report_progress = lambda x, y: x + + @synchronous('sync_lock') + def card_prefix(self, end_session=True): + self._debug() + return (None, None) + + @synchronous('sync_lock') + def total_space(self, end_session=True): + self._debug() + opcode, result = self._call_client('TOTAL_SPACE', {}) + if opcode == 'OK': + return (result['total_space_on_device'], 0, 0) + # protocol error if we get here + return (0, 0, 0) + + @synchronous('sync_lock') + def free_space(self, end_session=True): + self._debug() + opcode, result = self._call_client('FREE_SPACE', {}) + if opcode == 'OK': + return (result['free_space_on_device'], 0, 0) + # protocol error if we get here + return (0, 0, 0) + + @synchronous('sync_lock') + def books(self, oncard=None, end_session=True): + self._debug(oncard) + if oncard is not None: + return BookList(None, None, None) + opcode, result = self._call_client('GET_BOOK_COUNT', {}) + bl = BookList(None, self.PREFIX, self.settings) + if opcode == 'OK': + count = result['count'] + for i in range(0, count): + self._debug('retrieve metadata book', i) + opcode, result = self._call_client('GET_BOOK_METADATA', {'index': i}, + print_debug_info=False) + if opcode == 'OK': + if '_series_sort_' in result: + del result['_series_sort_'] + book = self.json_codec.raw_to_book(result, Book, self.PREFIX) + self._set_known_metadata(book) + bl.add_book(book, replace_metadata=True) + else: + raise IOError(_('Protocol error -- book metadata not returned')) + return bl + + @synchronous('sync_lock') + def sync_booklists(self, booklists, end_session=True): + self._debug() + # If we ever do device_db plugboards, this is where it will go. We will + # probably need to send two booklists, one with calibre's data that is + # given back by "books", and one that has been plugboarded. + self._call_client('SEND_BOOKLISTS', { 'count': len(booklists[0]) } ) + for i,book in enumerate(booklists[0]): + if not self._metadata_already_on_device(book): + self._set_known_metadata(book) + self._debug('syncing book', book.lpath) + opcode, result = self._call_client('SEND_BOOK_METADATA', + {'index': i, 'data': book}, + print_debug_info=False) + if opcode != 'OK': + self._debug('protocol error', opcode, i) + raise IOError(_('Protocol error -- sync_booklists')) + + @synchronous('sync_lock') + def eject(self): + self._debug() + if self.device_socket: + self.device_socket.close() + self.device_socket = None + self.is_connected = False + + @synchronous('sync_lock') + def post_yank_cleanup(self): + self._debug() + + @synchronous('sync_lock') + def upload_books(self, files, names, on_card=None, end_session=True, + metadata=None): + self._debug(names) + + paths = [] + names = iter(names) + metadata = iter(metadata) + + for i, infile in enumerate(files): + mdata, fname = metadata.next(), names.next() + lpath = self._create_upload_path(mdata, fname, create_dirs=False) + if not hasattr(infile, 'read'): + infile = USBMS.normalize_path(infile) + book = Book(self.PREFIX, lpath, other=mdata) + length = self._put_file(infile, lpath, book, i, len(files)) + if length < 0: + raise IOError(_('Sending book %s to device failed') % lpath) + paths.append((lpath, length)) + # No need to deal with covers. The client will get the thumbnails + # in the mi structure + self.report_progress((i+1) / float(len(files)), _('Transferring books to device...')) + + self.report_progress(1.0, _('Transferring books to device...')) + self._debug('finished uploading %d books'%(len(files))) + return paths + + @synchronous('sync_lock') + def add_books_to_metadata(self, locations, metadata, booklists): + self._debug('adding metadata for %d books'%(len(metadata))) + + metadata = iter(metadata) + for i, location in enumerate(locations): + self.report_progress((i+1) / float(len(locations)), + _('Adding books to device metadata listing...')) + info = metadata.next() + lpath = location[0] + length = location[1] + lpath = self._strip_prefix(lpath) + book = Book(self.PREFIX, lpath, other=info) + if book.size is None: + book.size = length + b = booklists[0].add_book(book, replace_metadata=True) + if b: + b._new_book = True + self.report_progress(1.0, _('Adding books to device metadata listing...')) + self._debug('finished adding metadata') + + @synchronous('sync_lock') + def delete_books(self, paths, end_session=True): + self._debug(paths) + for path in paths: + # the path has the prefix on it (I think) + path = self._strip_prefix(path) + opcode, result = self._call_client('DELETE_BOOK', {'lpath': path}) + if opcode == 'OK': + self._debug('removed book with UUID', result['uuid']) + else: + raise IOError(_('Protocol error - delete books')) + + @synchronous('sync_lock') + def remove_books_from_metadata(self, paths, booklists): + self._debug(paths) + for i, path in enumerate(paths): + path = self._strip_prefix(path) + self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...')) + for bl in booklists: + for book in bl: + if path == book.path: + bl.remove_book(book) + self._set_known_metadata(book, remove=True) + self.report_progress(1.0, _('Removing books from device metadata listing...')) + self._debug('finished removing metadata for %d books'%(len(paths))) + + + @synchronous('sync_lock') + def get_file(self, path, outfile, end_session=True): + self._debug(path) + eof = False + position = 0 + while not eof: + opcode, result = self._call_client('GET_BOOK_FILE_SEGMENT', + {'lpath' : path, 'position': position}, + print_debug_info=False ) + if opcode == 'OK': + if not result['eof']: + data = b64decode(result['data']) + if len(data) != result['next_position'] - position: + self._debug('position mismatch', result['next_position'], position) + position = result['next_position'] + outfile.write(data) + else: + eof = True + else: + raise IOError(_('request for book data failed')) + + @synchronous('sync_lock') + def set_plugboards(self, plugboards, pb_func): + self._debug() + self.plugboards = plugboards + self.plugboard_func = pb_func + + @synchronous('sync_lock') + def startup(self): + self.listen_socket = None + + @synchronous('sync_lock') + def startup_on_demand(self): + if getattr(self, 'listen_socket', None) is not None: + # we are already running + return + if len(self.opcodes) != len(self.reverse_opcodes): + self._debug(self.opcodes, self.reverse_opcodes) + self.is_connected = False + self.listen_socket = None + self.device_socket = None + self.json_codec = JsonCodec() + self.known_metadata = {} + self.debug_time = time.time() + self.debug_start_time = time.time() + self.max_book_packet_len = 0 + self.noop_counter = 0 + try: + self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except: + self._debug('creation of listen socket failed') + return + + for i in range(0, 100): # try up to 100 random port numbers + port = random.randint(8192, 32000) + try: + self._debug('try port', port) + self.listen_socket.bind(('', port)) + break + except socket.error: + port = 0 + except: + self._debug('Unknown exception while allocating listen socket') + traceback.print_exc() + raise + if port == 0: + self._debug('Failed to allocate a port'); + self.listen_socket.close() + self.listen_socket = None + return + + try: + self.listen_socket.listen(0) + except: + self._debug('listen on socket failed', port) + self.listen_socket.close() + self.listen_socket = None + return + + try: + publish_zeroconf('calibre smart device client', + '_calibresmartdeviceapp._tcp', port, {}) + except: + self._debug('registration with bonjour failed') + self.listen_socket.close() + self.listen_socket = None + return + + self._debug('listening on port', port) + self.port = port + + @synchronous('sync_lock') + def shutdown(self): + if getattr(self, 'listen_socket', None) is not None: + self.listen_socket.close() + self.listen_socket = None + unpublish_zeroconf('calibre smart device client', + '_calibresmartdeviceapp._tcp', self.port, {}) + + # Methods for dynamic control + + @synchronous('sync_lock') + def is_dynamically_controllable(self): + return 'smartdevice' + + @synchronous('sync_lock') + def start_plugin(self): + self.startup_on_demand() + + @synchronous('sync_lock') + def stop_plugin(self): + self.shutdown() + + @synchronous('sync_lock') + def get_option(self, opt_string, default=None): + opt = self._get_smartdevice_option_number(opt_string) + if opt is not None: + return self.settings().extra_customization[opt] + return default + + @synchronous('sync_lock') + def set_option(self, opt_string, value): + opt = self._get_smartdevice_option_number(opt_string) + if opt is not None: + config = self._configProxy() + ec = config['extra_customization'] + ec[opt] = value + config['extra_customization'] = ec + + @synchronous('sync_lock') + def is_running(self): + return getattr(self, 'listen_socket', None) is not None + + From 60d7f4a5c6789b059f88ad02b91f1e2a05c65401 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 31 Jul 2012 09:48:51 +0200 Subject: [PATCH 26/34] Remove password debugging code. --- src/calibre/devices/smart_device_app/driver.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index b3a2f90ecb..d11e509032 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -501,8 +501,7 @@ class SMART_DEVICE_APP (DeviceConfig, DevicePlugin): 'validExtensions': self.ALL_FORMATS, 'passwordChallenge': challenge, 'currentLibraryName': self.current_library_name, - 'currentLibraryUUID': library_uuid, - 'rightAnswer': hash_digest}) + 'currentLibraryUUID': library_uuid}) if opcode != 'OK': # Something wrong with the return. Close the socket # and continue. From 1cd9e7f9738c170fbe73bcfe47c84ea5ea87e427 Mon Sep 17 00:00:00 2001 From: Oliver Graf Date: Tue, 31 Jul 2012 11:43:33 +0200 Subject: [PATCH 27/34] Added cover page removal (only explicit named), picture anchored to page warning and series information. --- manual/conversion.rst | 6 ++- src/calibre/ebooks/metadata/odt.py | 56 ++++++++++++--------- src/calibre/ebooks/odt/input.py | 78 +++++++++++++++++++++++++----- 3 files changed, 104 insertions(+), 36 deletions(-) diff --git a/manual/conversion.rst b/manual/conversion.rst index a4ecd902cc..feae2a4273 100644 --- a/manual/conversion.rst +++ b/manual/conversion.rst @@ -734,7 +734,11 @@ If this property is detected by |app|, the following custom properties are recog opf.pubdate opf.isbn opf.language + opf.series + opf.seriesindex In addition to this, you can specify the picture to use as the cover by naming it ``opf.cover`` (right click, Picture->Options->Name) in the ODT. If no picture with this name is found, the 'smart' method is used. -To prevent this you can set the custom property ``opf.nocover`` ('Yes or No' type) to Yes. +As the cover detection might result in double covers in certain output formats, the process will remove the paragraph (only if the only content is the cover!) from the document. But this works only with the named picture! + +To disable cover detection you can set the custom property ``opf.nocover`` ('Yes or No' type) to Yes in advanced mode. diff --git a/src/calibre/ebooks/metadata/odt.py b/src/calibre/ebooks/metadata/odt.py index a4371a4506..9929b9e375 100644 --- a/src/calibre/ebooks/metadata/odt.py +++ b/src/calibre/ebooks/metadata/odt.py @@ -196,6 +196,13 @@ def get_metadata(stream, extract_cover=True): mi.publisher = data['opf.publisher'] if data.get('opf.pubdate', ''): mi.pubdate = parse_date(data['opf.pubdate'], assume_utc=True) + if data.get('opf.series', ''): + mi.series = data['opf.series'] + if data.get('opf.seriesindex', ''): + try: + mi.series_index = int(data['opf.seriesindex']) + except ValueError: + mi.series_index = 1 if data.get('opf.language', ''): cl = canonicalize_lang(data['opf.language']) if cl: @@ -206,7 +213,7 @@ def get_metadata(stream, extract_cover=True): read_cover(stream, zin, mi, opfmeta, extract_cover) except: pass # Do not let an error reading the cover prevent reading other data - + return mi def read_cover(stream, zin, mi, opfmeta, extract_cover): @@ -216,34 +223,35 @@ def read_cover(stream, zin, mi, opfmeta, extract_cover): otext = odLoad(stream) cover_href = None cover_data = None - # check that it's really a ODT - if otext.mimetype == u'application/vnd.oasis.opendocument.text': - for elem in otext.text.getElementsByType(odFrame): - img = elem.getElementsByType(odImage) - if len(img) > 0: # there should be only one - i_href = img[0].getAttribute('href') - try: - raw = zin.read(i_href) - except KeyError: - continue - try: - width, height, fmt = identify_data(raw) - except: - continue - else: + cover_frame = None + for frm in otext.topnode.getElementsByType(odFrame): + img = frm.getElementsByType(odImage) + if len(img) > 0: # there should be only one + i_href = img[0].getAttribute('href') + try: + raw = zin.read(i_href) + except KeyError: continue - if opfmeta and elem.getAttribute('name').lower() == u'opf.cover': - cover_href = i_href - cover_data = (fmt, raw) + try: + width, height, fmt = identify_data(raw) + except: + continue + else: + continue + if opfmeta and frm.getAttribute('name').lower() == u'opf.cover': + cover_href = i_href + cover_data = (fmt, raw) + cover_frame = frm.getAttribute('name') # could have upper case + break + if cover_href is None and 0.8 <= height/width <= 1.8 and height*width >= 12000: + cover_href = i_href + cover_data = (fmt, raw) + if not opfmeta: break - if cover_href is None and 0.8 <= height/width <= 1.8 and height*width >= 12000: - cover_href = i_href - cover_data = (fmt, raw) - if not opfmeta: - break if cover_href is not None: mi.cover = cover_href + mi.odf_cover_frame = cover_frame if extract_cover: if not cover_data: raw = zin.read(cover_href) diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py index 1a70335a13..85a4775f9f 100644 --- a/src/calibre/ebooks/odt/input.py +++ b/src/calibre/ebooks/odt/input.py @@ -10,6 +10,9 @@ import os from lxml import etree from odf.odf2xhtml import ODF2XHTML +from odf.opendocument import load as odLoad +from odf.draw import Frame as odFrame, Image as odImage +from odf.namespaces import TEXTNS as odTEXTNS from calibre import CurrentDir, walk @@ -138,9 +141,63 @@ class Extract(ODF2XHTML): r.selectorText = '.'+replace_name return sheet.cssText, sel_map + def search_page_img(self, mi, log): + for frm in self.document.topnode.getElementsByType(odFrame): + try: + if frm.getAttrNS(odTEXTNS,u'anchor-type') == 'page': + log.warn('Document has Pictures anchored to Page, will all end up before first page!') + break + except ValueError: + pass + + def filter_cover(self, mi, log): + # filter the Element tree (remove the detected cover) + if mi.cover and mi.odf_cover_frame: + for frm in self.document.topnode.getElementsByType(odFrame): + # search the right frame + if frm.getAttribute('name') == mi.odf_cover_frame: + img = frm.getElementsByType(odImage) + # only one draw:image allowed in the draw:frame + if len(img) == 1 and img[0].getAttribute('href') == mi.cover: + # ok, this is the right frame with the right image + # check if there are more childs + if len(frm.childNodes) != 1: + break + # check if the parent paragraph more childs + para = frm.parentNode + if para.tagName != 'text:p' or len(para.childNodes) != 1: + break + # now it should be safe to remove the text:p + parent = para.parentNode + parent.removeChild(para) + log("Removed cover image paragraph from document...") + break + + def filter_load(self, odffile, mi, log): + """ This is an adaption from ODF2XHTML. It adds a step between + load and parse of the document where the Element tree can be + modified. + """ + # first load the odf structure + self.lines = [] + self._wfunc = self._wlines + if isinstance(odffile, basestring) \ + or hasattr(odffile, 'read'): # Added by Kovid + self.document = odLoad(odffile) + else: + self.document = odffile + # filter stuff + self.search_page_img(mi, log) + try: + self.filter_cover(mi, log) + except: + pass + # parse the modified tree and generate xhtml + self._walknode(self.document.topnode) + def __call__(self, stream, odir, log): from calibre.utils.zipfile import ZipFile - from calibre.ebooks.metadata.meta import get_metadata + from calibre.ebooks.metadata.odt import get_metadata from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.customize.ui import quick_metadata @@ -148,12 +205,20 @@ class Extract(ODF2XHTML): os.makedirs(odir) with CurrentDir(odir): log('Extracting ODT file...') - html = self.odf2xhtml(stream) + mi = get_metadata(stream, 'odt') + if not mi.title: + mi.title = _('Unknown') + if not mi.authors: + mi.authors = [_('Unknown')] + self.filter_load(stream, mi, log) + html = self.xhtml() # A blanket img specification like this causes problems # with EPUB output as the containing element often has # an absolute height and width set that is larger than # the available screen real estate html = html.replace('img { width: 100%; height: 100%; }', '') + # odf2xhtml creates empty title tag + html = html.replace('','%s'%(mi.title,)) try: html = self.fix_markup(html, log) except: @@ -162,15 +227,6 @@ class Extract(ODF2XHTML): f.write(html.encode('utf-8')) zf = ZipFile(stream, 'r') self.extract_pictures(zf) - stream.seek(0) - with quick_metadata: - # We dont want the cover, as it will lead to a duplicated image - # if no external cover is specified. - mi = get_metadata(stream, 'odt') - if not mi.title: - mi.title = _('Unknown') - if not mi.authors: - mi.authors = [_('Unknown')] opf = OPFCreator(os.path.abspath(os.getcwdu()), mi) opf.create_manifest([(os.path.abspath(f), None) for f in walk(os.getcwdu())]) From c1173a2224f76d5a46a76c5e1378400ef64daa74 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 Jul 2012 16:21:13 +0530 Subject: [PATCH 28/34] Update podofo to 0.9.1 in all binary build, to fix corruption of some PDFs when updating metadata. Fixes #1031086 (Save To Disk with Update Metadata can corrupt PDF) --- setup/extensions.py | 2 +- setup/installer/linux/freeze2.py | 2 +- setup/installer/osx/app/main.py | 9 +-------- setup/installer/windows/notes.rst | 19 +------------------ 4 files changed, 4 insertions(+), 28 deletions(-) diff --git a/setup/extensions.py b/setup/extensions.py index e4054e87fa..4dd76be3a6 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -140,7 +140,7 @@ extensions = [ ['calibre/utils/podofo/podofo.cpp'], libraries=['podofo'], lib_dirs=[podofo_lib], - inc_dirs=[podofo_inc], + inc_dirs=[podofo_inc, os.path.dirname(podofo_inc)], optional=True, error=podofo_error), diff --git a/setup/installer/linux/freeze2.py b/setup/installer/linux/freeze2.py index 6ecb21768f..8a8fa06ee1 100644 --- a/setup/installer/linux/freeze2.py +++ b/setup/installer/linux/freeze2.py @@ -32,7 +32,7 @@ binary_includes = [ '/usr/lib/libunrar.so', '/usr/lib/libsqlite3.so.0', '/usr/lib/libmng.so.1', - '/usr/lib/libpodofo.so.0.8.4', + '/usr/lib/libpodofo.so.0.9.1', '/lib/libz.so.1', '/usr/lib/libtiff.so.5', '/lib/libbz2.so.1', diff --git a/setup/installer/osx/app/main.py b/setup/installer/osx/app/main.py index 8d3853ea28..504f7fc49a 100644 --- a/setup/installer/osx/app/main.py +++ b/setup/installer/osx/app/main.py @@ -243,9 +243,6 @@ class Py2App(object): @flush def get_local_dependencies(self, path_to_lib): for x in self.get_dependencies(path_to_lib): - if x.startswith('libpodofo'): - yield x, x - continue for y in (SW+'/lib/', '/usr/local/lib/', SW+'/qt/lib/', '/opt/local/lib/', SW+'/python/Python.framework/', SW+'/freetype/lib/'): @@ -330,10 +327,6 @@ class Py2App(object): for f in glob.glob('src/calibre/plugins/*.so'): shutil.copy2(f, dest) self.fix_dependencies_in_lib(join(dest, basename(f))) - if 'podofo' in f: - self.change_dep('libpodofo.0.8.4.dylib', - self.FID+'/'+'libpodofo.0.8.4.dylib', join(dest, basename(f))) - @flush def create_plist(self): @@ -380,7 +373,7 @@ class Py2App(object): @flush def add_podofo(self): info('\nAdding PoDoFo') - pdf = join(SW, 'lib', 'libpodofo.0.8.4.dylib') + pdf = join(SW, 'lib', 'libpodofo.0.9.1.dylib') self.install_dylib(pdf) @flush diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index 8cf55cef78..e29b205de6 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -322,24 +322,7 @@ cp build/podofo-*/build/src/Release/podofo.exp lib/ cp build/podofo-*/build/podofo_config.h include/podofo/ cp -r build/podofo-*/src/* include/podofo/ -You have to use >=0.8.2 - -The following patch (against -r1269) was required to get it to compile: - - -Index: src/PdfFiltersPrivate.cpp -=================================================================== ---- src/PdfFiltersPrivate.cpp (revision 1261) -+++ src/PdfFiltersPrivate.cpp (working copy) -@@ -1019,7 +1019,7 @@ - /* - * Prepare for input from a memory buffer. - */ --GLOBAL(void) -+void - jpeg_memory_src (j_decompress_ptr cinfo, const JOCTET * buffer, size_t bufsize) - { - my_src_ptr src; +You have to use >=0.9.1 ImageMagick From cbdb05978b8be7764ed3efd9f773b570df58b7bb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 Jul 2012 17:41:44 +0530 Subject: [PATCH 29/34] Use context manager instead of acquire()/release() --- src/calibre/devices/smart_device_app/driver.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index c63c04b7a6..8f59d310c8 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -36,13 +36,9 @@ def synchronous(tlockname): def _synched(func): @wraps(func) - def _synchronizer(self,*args, **kwargs): - tlock = self.__getattribute__( tlockname) - tlock.acquire() - try: + def _synchronizer(self, *args, **kwargs): + with self.__getattribute__(tlockname): return func(self, *args, **kwargs) - finally: - tlock.release() return _synchronizer return _synched From 4b13f6079b4c08c5aa9855651c96ecb501377d81 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 Jul 2012 17:44:29 +0530 Subject: [PATCH 30/34] ... --- src/calibre/devices/kindle/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index a12ad5ebce..1971faef60 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -13,7 +13,6 @@ import datetime, os, re, sys, json, hashlib from calibre.devices.kindle.bookmark import Bookmark from calibre.devices.usbms.driver import USBMS from calibre import strftime -from calibre.utils.logging import default_log ''' Notes on collections: @@ -389,6 +388,7 @@ class KINDLE2(KINDLE): self.upload_apnx(path, filename, metadata, filepath) def upload_kindle_thumbnail(self, metadata, filepath): + from calibre.utils.logging import default_log coverdata = getattr(metadata, 'thumbnail', None) if not coverdata or not coverdata[2]: return From ca05e82c1bcc7e09928bb789250801974b5a33f8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 Jul 2012 17:55:54 +0530 Subject: [PATCH 31/34] ... --- src/calibre/devices/smart_device_app/driver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 8f59d310c8..5a2833ef7e 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -460,12 +460,12 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): ans = select.select((self.listen_socket,), (), (), 0) if len(ans[0]) > 0: # timeout in 10 ms to detect rare case where the socket went - # way between the select and the accent + # way between the select and the accept try: self.device_socket = None self.listen_socket.settimeout(0.010) - self.device_socket, ign = \ - eintr_retry_call(self.listen_socket.accept) + self.device_socket, ign = eintr_retry_call( + self.listen_socket.accept) self.listen_socket.settimeout(None) self.device_socket.settimeout(None) self.is_connected = True From 324ae47a3752c7e9977e304735d1663a53657e0a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 31 Jul 2012 15:15:55 +0200 Subject: [PATCH 32/34] Kovid's comments #1 --- .../devices/smart_device_app/driver.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index c63c04b7a6..daab9bcb48 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -137,10 +137,13 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): return total_elapsed = time.time() - self.debug_start_time elapsed = time.time() - self.debug_time - prints('SMART_DEV (%7.2f:%7.3f) %s'%(total_elapsed, elapsed, + print('SMART_DEV (%7.2f:%7.3f) %s'%(total_elapsed, elapsed, inspect.stack()[1][3]), end='') for a in args: - prints(a, end='') + try: + prints('', a, end='') + except: + prints('', 'value too long', end='') print() self.debug_time = time.time() @@ -285,16 +288,16 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): return json.dumps([op, res], encoding='utf-8') # Network functions - def _read_string_from_net(self, conn): + def _read_string_from_net(self): data = bytes(0) while True: dex = data.find('[') if dex >= 0: break - # conn.recv seems to return a pointer into some internal buffer. + # recv seems to return a pointer into some internal buffer. # Things get trashed if we don't make a copy of the data. self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) - v = conn.recv(self.BASE_PACKET_LEN) + v = self.device_socket.recv(self.BASE_PACKET_LEN) self.device_socket.settimeout(None) if len(v) == 0: return '' # documentation says the socket is broken permanently. @@ -304,7 +307,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): pos = len(data) while pos < total_len: self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) - v = conn.recv(total_len - pos) + v = self.device_socket.recv(total_len - pos) self.device_socket.settimeout(None) if len(v) == 0: return '' # documentation says the socket is broken permanently. @@ -330,7 +333,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) self.device_socket.sendall(('%d' % len(s))+s) self.device_socket.settimeout(None) - v = self._read_string_from_net(self.device_socket) + v = self._read_string_from_net() if print_debug_info and extra_debug: self._debug('received string', v) if v: @@ -452,13 +455,12 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): # through and actually try to talk to the client. try: # This will usually toss an exception if the socket is gone. - try: - if self._call_client('NOOP', dict())[0] is None: - self.is_connected = False - except: + if self._call_client('NOOP', dict())[0] is None: self.is_connected = False except: self.is_connected = False + if not self.is_connected: + self.device_socket.close() return (self.is_connected, self) if getattr(self, 'listen_socket', None) is not None: ans = select.select((self.listen_socket,), (), (), 0) From 49cb868bb3b0b0659986e1c3ad442c0c89ccc4af Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 31 Jul 2012 15:25:46 +0200 Subject: [PATCH 33/34] Use a str type instead of unicode when searching for the opening '[' in the network packet. Using unicode seems to work, but it really is a byte. --- src/calibre/devices/smart_device_app/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index daab9bcb48..bae5d82ba3 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -291,7 +291,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): def _read_string_from_net(self): data = bytes(0) while True: - dex = data.find('[') + dex = data.find(b'[') if dex >= 0: break # recv seems to return a pointer into some internal buffer. From 99de8e6cc3cc246191985ff5c9cc898e3edaa56f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 Jul 2012 21:17:42 +0530 Subject: [PATCH 34/34] prints() shouldn't throw an exception on windows when printing out very large objects --- src/calibre/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 7d4db1e512..a0f3b49498 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -201,7 +201,8 @@ def prints(*args, **kwargs): try: file.write(arg) except: - file.write(repr(arg)) + import repr as reprlib + file.write(reprlib.repr(arg)) if i != len(args)-1: file.write(bytes(sep)) file.write(bytes(end))

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