From 3e0c43283d54a36c2574801070171cee33d974af Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 18 Apr 2011 21:43:10 +0100 Subject: [PATCH 01/32] Change author_sort_copy_method default from invert to comma. --- resources/default_tweaks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index c4c951f980..091aa9a34d 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -48,7 +48,7 @@ authors_completer_append_separator = False # When this tweak is changed, the author_sort values stored with each author # must be recomputed by right-clicking on an author in the left-hand tags pane, # selecting 'manage authors', and pressing 'Recalculate all author sort values'. -author_sort_copy_method = 'invert' +author_sort_copy_method = 'comma' #: Use author sort in Tag Browser # Set which author field to display in the tags pane (the list of authors, From 5312b49d85132787bc4f18d91fa7a8230561e298 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 08:59:53 +0100 Subject: [PATCH 02/32] Divide ratings by 2 when used in a template. --- src/calibre/ebooks/metadata/book/base.py | 2 ++ src/calibre/library/save_to_disk.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 167ae52fa3..57c267319f 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -625,6 +625,8 @@ class Metadata(object): res = res + ' [%s]'%self.format_series_index() elif datatype == 'datetime': res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) + elif datatype == 'rating': + res = res/2 return (name, unicode(res), orig_res, fmeta) return (None, None, None, None) diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index 3c57af40a8..42e6c8b156 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -198,7 +198,6 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250, for key in custom_metadata: if key in format_args: cm = custom_metadata[key] - ## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't... if cm['datatype'] == 'series': format_args[key] = title_sort(format_args[key], order=tsorder) if key+'_index' in format_args: From 1e1b530113636d344c59e2c93fdabc3ca974a5cf Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 09:46:43 +0100 Subject: [PATCH 03/32] Add the ability to get the calibre field 'size' using a formatter function booksize() --- src/calibre/library/database2.py | 1 + src/calibre/manual/template_lang.rst | 1 + src/calibre/utils/formatter_functions.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index bdcefd13a2..d585c60aef 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -853,6 +853,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): mi.pubdate = row[fm['pubdate']] mi.uuid = row[fm['uuid']] mi.title_sort = row[fm['sort']] + mi.book_size = row[fm['size']] mi.last_modified = row[fm['last_modified']] formats = row[fm['formats']] if not formats: diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index cdb8df2e2b..a77f0d1697 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -230,6 +230,7 @@ The following functions are available in addition to those described in single-f * ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers. * ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression + * ``booksize()`` -- returns the value of the |app| 'size' field. Returns '' if there are no formats. * ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers. * ``field(name)`` -- returns the metadata field named by ``name``. diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 7957bd0749..aa8e4fb3a3 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -549,8 +549,22 @@ class BuiltinCapitalize(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return capitalize(val) +class BuiltinBooksize(BuiltinFormatterFunction): + name = 'booksize' + arg_count = 0 + doc = _('booksize() -- return value of the field capitalized') + + def evaluate(self, formatter, kwargs, mi, locals): + if mi.book_size is not None: + try: + return str(mi.book_size) + except: + pass + return '' + builtin_add = BuiltinAdd() builtin_assign = BuiltinAssign() +builtin_booksize = BuiltinBooksize() builtin_capitalize = BuiltinCapitalize() builtin_cmp = BuiltinCmp() builtin_contains = BuiltinContains() From d95d6eca67f7fc6f6b06cf2ab8d70477ffec9048 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 10:07:10 +0100 Subject: [PATCH 04/32] Improve performance of get_categories. --- src/calibre/library/database2.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index d585c60aef..bc0a8235e4 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1379,13 +1379,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for (cat, dex, mult, is_comp) in md: if not book[dex]: continue + tid_cat = tids[cat] + tcats_cat = tcategories[cat] if not mult: val = book[dex] if is_comp: - item = tcategories[cat].get(val, None) + item = tcats_cat.get(val, None) if not item: item = tag_class(val, val) - tcategories[cat][val] = item + tcats_cat[val] = item item.c += 1 item.id = val if rating > 0: @@ -1393,11 +1395,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): item.rc += 1 continue try: - (item_id, sort_val) = tids[cat][val] # let exceptions fly - item = tcategories[cat].get(val, None) + (item_id, sort_val) = tid_cat[val] # let exceptions fly + item = tcats_cat.get(val, None) if not item: item = tag_class(val, sort_val) - tcategories[cat][val] = item + tcats_cat[val] = item item.c += 1 item.id_set.add(book[0]) item.id = item_id @@ -1411,21 +1413,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if is_comp: vals = [v.strip() for v in vals if v.strip()] for val in vals: - if val not in tids: - tids[cat][val] = (val, val) - item = tcategories[cat].get(val, None) - if not item: - item = tag_class(val, val) - tcategories[cat][val] = item - item.c += 1 - item.id = val + if val not in tid_cat: + tid_cat[val] = (val, val) for val in vals: try: - (item_id, sort_val) = tids[cat][val] # let exceptions fly - item = tcategories[cat].get(val, None) + (item_id, sort_val) = tid_cat[val] # let exceptions fly + item = tcats_cat.get(val, None) if not item: item = tag_class(val, sort_val) - tcategories[cat][val] = item + tcats_cat[val] = item item.c += 1 item.id_set.add(book[0]) item.id = item_id From b4dda2792357736ec155412f46facce563afb274 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 13:42:09 +0100 Subject: [PATCH 05/32] Very strange change to prevent calibre from dieing when device_metadata_available or device_connection_changed is emitted. --- src/calibre/gui2/device.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 99cb5848ba..d00aa031f5 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -7,7 +7,7 @@ import os, traceback, Queue, time, cStringIO, re, sys from threading import Thread from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \ - Qt, pyqtSignal, QDialog + Qt, pyqtSignal, QDialog, QObject from calibre.customize.ui import available_input_formats, available_output_formats, \ device_plugins @@ -587,8 +587,7 @@ class DeviceMenu(QMenu): # {{{ # }}} -class DeviceMixin(object): # {{{ - +class DeviceSignals(QObject): #: This signal is emitted once, after metadata is downloaded from the #: connected device. #: The sequence: gui.device_manager.is_device_connected will become True, @@ -599,6 +598,10 @@ class DeviceMixin(object): # {{{ device_metadata_available = pyqtSignal() device_connection_changed = pyqtSignal(object) +device_signals = DeviceSignals() + +class DeviceMixin(object): # {{{ + def __init__(self): self.device_error_dialog = error_dialog(self, _('Error'), _('Error communicating with device'), ' ') @@ -745,7 +748,7 @@ class DeviceMixin(object): # {{{ self.location_manager.update_devices() self.library_view.set_device_connected(self.device_connected) self.refresh_ondevice() - self.device_connection_changed.emit(connected) + device_signals.device_connection_changed.emit(connected) def info_read(self, job): ''' @@ -784,7 +787,7 @@ class DeviceMixin(object): # {{{ self.sync_news() self.sync_catalogs() self.refresh_ondevice() - self.device_metadata_available.emit() + device_signals.device_metadata_available.emit() def refresh_ondevice(self, reset_only = False): ''' From d7f17e9a1975af262e2c894bfa0815cd04fdb9c1 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 14:28:59 +0100 Subject: [PATCH 06/32] Improvements to using the current search as a restriction --- src/calibre/gui2/layout.py | 2 -- src/calibre/gui2/search_restriction_mixin.py | 33 ++++++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index c72b074463..7250103615 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -156,8 +156,6 @@ class SearchBar(QWidget): # {{{ x = ComboBoxWithHelp(self) x.setMaximumSize(QSize(150, 16777215)) x.setObjectName("search_restriction") - x.setToolTip(_('Books display will be restricted to those matching the ' - 'selected saved search')) l.addWidget(x) parent.search_restriction = x diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 8ef02b34b0..ffebc9e131 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -17,6 +17,10 @@ class SearchRestrictionMixin(object): self.search_restriction.setMinimumContentsLength(10) self.search_restriction.setStatusTip(self.search_restriction.toolTip()) self.search_count.setText(_("(all books)")) + self.search_restriction_tooltip = \ + _('Books display will be restricted to those matching a ' + 'selected saved search') + self.search_restriction.setToolTip(self.search_restriction_tooltip) def apply_named_search_restriction(self, name): if not name: @@ -30,29 +34,38 @@ class SearchRestrictionMixin(object): self.apply_search_restriction(r) def apply_text_search_restriction(self, search): + search = unicode(search) if not search: - self.search_restriction.setItemText(1, _('*Current search')) self.search_restriction.setCurrentIndex(0) else: - self.search_restriction.setCurrentIndex(1) - self.search_restriction.setItemText(1, search) + s = '*' + search + if self.search_restriction.count() > 1: + txt = unicode(self.search_restriction.itemText(2)) + if txt.startswith('*'): + self.search_restriction.setItemText(2, s) + else: + self.search_restriction.insertItem(2, s) + else: + self.search_restriction.insertItem(2, s) + self.search_restriction.setCurrentIndex(2) + self.search_restriction.setToolTip('

' + + self.search_restriction_tooltip + + _(' or the search ') + "'" + search + "'

") self._apply_search_restriction(search) def apply_search_restriction(self, i): - self.search_restriction.setItemText(1, _('*Current search')) if i == 1: - restriction = unicode(self.search.currentText()) - if not restriction: - self.search_restriction.setCurrentIndex(0) - else: - self.search_restriction.setItemText(1, restriction) + self.apply_text_search_restriction(unicode(self.search.currentText())) + elif i == 2 and unicode(self.search_restriction.currentText()).startswith('*'): + self.apply_text_search_restriction( + unicode(self.search_restriction.currentText())[1:]) else: r = unicode(self.search_restriction.currentText()) if r is not None and r != '': restriction = 'search:"%s"'%(r) else: restriction = '' - self._apply_search_restriction(restriction) + self._apply_search_restriction(restriction) def _apply_search_restriction(self, restriction): self.saved_search.clear() From 640c4ff7848ffc7944b970f480dc493a4f4e1060 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 15:02:37 +0100 Subject: [PATCH 07/32] Fix non-escaped '|' when searching for commas in authors using REGEXP_MATCH --- src/calibre/library/caches.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 9523795f28..92c5ca9b3c 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -707,7 +707,10 @@ class ResultCache(SearchQueryParser): # {{{ for loc in location: # location is now an array of field indices if loc == db_col['authors']: ### DB stores authors with commas changed to bars, so change query - q = query.replace(',', '|'); + if matchkind == REGEXP_MATCH: + q = query.replace(',', r'\|'); + else: + q = query.replace(',', '|'); else: q = query From ccaa7143b5e23785c683f568518d3b2942c270bc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 19 Apr 2011 09:45:42 -0600 Subject: [PATCH 08/32] timing worker launches --- src/calibre/utils/ipc/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/utils/ipc/server.py b/src/calibre/utils/ipc/server.py index e3b7bfd449..5bbc6bafc9 100644 --- a/src/calibre/utils/ipc/server.py +++ b/src/calibre/utils/ipc/server.py @@ -113,6 +113,7 @@ class Server(Thread): self.start() def launch_worker(self, gui=False, redirect_output=None): + #start = time.time() with self._worker_launch_lock: self.launched_worker_count += 1 id = self.launched_worker_count @@ -136,6 +137,7 @@ class Server(Thread): break if isinstance(cw, basestring): raise CriticalError('Failed to launch worker process:\n'+cw) + #print 'Launch took:', time.time() - start return cw def do_launch(self, env, gui, redirect_output, rfile): From 6b52f4ad89a14c625d084050abda4ec222195aea Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 19 Apr 2011 11:24:51 -0600 Subject: [PATCH 09/32] Launch worker processes on demand --- src/calibre/gui2/__init__.py | 4 +++- src/calibre/gui2/preferences/misc.py | 14 +++++++++++--- src/calibre/gui2/preferences/misc.ui | 10 ++-------- src/calibre/manual/faq.rst | 11 ----------- src/calibre/utils/ipc/server.py | 25 +++++++------------------ 5 files changed, 23 insertions(+), 41 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 773aea3002..3ad8a81ffa 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -156,7 +156,9 @@ def _config(): c.add_opt('plugin_search_history', default=[], help='Search history for the recipe scheduler') c.add_opt('worker_limit', default=6, - help=_('Maximum number of waiting worker processes')) + help=_( + 'Maximum number of simultaneous conversion/news download jobs. ' + 'This number is twice the actual value for historical reasons.')) c.add_opt('get_social_metadata', default=True, help=_('Download social metadata (tags/rating/etc.)')) c.add_opt('overwrite_author_title_metadata', default=True, diff --git a/src/calibre/gui2/preferences/misc.py b/src/calibre/gui2/preferences/misc.py index 330332a716..ead5da4ce4 100644 --- a/src/calibre/gui2/preferences/misc.py +++ b/src/calibre/gui2/preferences/misc.py @@ -6,19 +6,27 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from calibre.gui2.preferences import ConfigWidgetBase, test_widget +from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting from calibre.gui2.preferences.misc_ui import Ui_Form from calibre.gui2 import error_dialog, config, open_local_file, info_dialog from calibre.constants import isosx -# Check Integrity {{{ +class WorkersSetting(Setting): + + def set_gui_val(self, val): + val = val//2 + Setting.set_gui_val(self, val) + + def get_gui_val(self): + val = Setting.get_gui_val(self) + return val * 2 class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): self.gui = gui r = self.register - r('worker_limit', config, restart_required=True) + r('worker_limit', config, restart_required=True, setting=WorkersSetting) r('enforce_cpu_limit', config, restart_required=True) self.device_detection_button.clicked.connect(self.debug_device_detection) self.button_open_config_dir.clicked.connect(self.open_config_dir) diff --git a/src/calibre/gui2/preferences/misc.ui b/src/calibre/gui2/preferences/misc.ui index c036cb971b..8b0189b0a1 100644 --- a/src/calibre/gui2/preferences/misc.ui +++ b/src/calibre/gui2/preferences/misc.ui @@ -17,7 +17,7 @@ - &Maximum number of waiting worker processes (needs restart): + Max. simultaneous conversion/news download jobs: opt_worker_limit @@ -27,13 +27,7 @@ - 2 - - - 10000 - - - 2 + 1 diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index ef4da23826..3dce13f144 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -549,17 +549,6 @@ How do I run calibre from my USB stick? A portable version of calibre is available at: `portableapps.com `_. However, this is usually out of date. You can also setup your own portable calibre install by following :ref:`these instructions `. -Why are there so many calibre-parallel processes on my system? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -|app| maintains two separate worker process pools. One is used for adding books/saving to disk and the other for conversions. You can control the number of worker processes via :guilabel:`Preferences->Advanced->Miscellaneous`. So if you set it to 6 that means a maximum of 3 conversions will run simultaneously. And that is why you will see the number of worker processes changes by two when you use the up and down arrows. On windows, you can set the priority that these processes run with. This can be useful on older, single CPU machines, if you find them slowing down to a crawl when conversions are running. - -In addition to this some conversion plugins run tasks in their own pool of processes, so for example if you bulk convert comics, each comic conversion will use three separate processes to render the images. The job manager knows this so it will run only a single comic conversion simultaneously. - -And since I'm sure someone will ask: The reason adding/saving books are in separate processes is because of PDF. PDF processing libraries can crash on reading PDFs and I dont want the crash to take down all of calibre. Also when adding EPUB books, in order to extract the cover you have to sometimes render the HTML of the first page, which means that it either has to run in the GUI thread of the main process or in a separate process. - -Finally, the reason calibre keep workers alive and idle instead of launching on demand is to workaround the slow startup time of python processes. - How do I run parts of |app| like news download and the content server on my own linux server? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/calibre/utils/ipc/server.py b/src/calibre/utils/ipc/server.py index 5bbc6bafc9..ea6ce88ad6 100644 --- a/src/calibre/utils/ipc/server.py +++ b/src/calibre/utils/ipc/server.py @@ -17,7 +17,7 @@ from binascii import hexlify from calibre.utils.ipc.launch import Worker from calibre.utils.ipc.worker import PARALLEL_FUNCS from calibre import detect_ncpus as cpu_count -from calibre.constants import iswindows +from calibre.constants import iswindows, DEBUG from calibre.ptempfile import base_dir _counter = 0 @@ -106,14 +106,14 @@ class Server(Thread): self.add_jobs_queue, self.changed_jobs_queue = Queue(), Queue() self.kill_queue = Queue() self.waiting_jobs = [] - self.pool, self.workers = deque(), deque() + self.workers = deque() self.launched_worker_count = 0 self._worker_launch_lock = RLock() self.start() def launch_worker(self, gui=False, redirect_output=None): - #start = time.time() + start = time.time() with self._worker_launch_lock: self.launched_worker_count += 1 id = self.launched_worker_count @@ -137,7 +137,8 @@ class Server(Thread): break if isinstance(cw, basestring): raise CriticalError('Failed to launch worker process:\n'+cw) - #print 'Launch took:', time.time() - start + if DEBUG: + print 'Worker Launch took:', time.time() - start return cw def do_launch(self, env, gui, redirect_output, rfile): @@ -206,13 +207,6 @@ class Server(Thread): job.duration = time.time() - job.start_time self.changed_jobs_queue.put(job) - # Start new workers - if len(self.pool) + len(self.workers) < self.pool_size: - try: - self.pool.append(self.launch_worker()) - except Exception: - pass - # Start waiting jobs sj = self.suitable_waiting_job() if sj is not None: @@ -224,7 +218,7 @@ class Server(Thread): job.killed = job.failed = True job.result = None else: - worker = self.pool.pop() + worker = self.launch_worker() worker.start_job(job) self.workers.append(worker) job.log_path = worker.log_path @@ -238,7 +232,7 @@ class Server(Thread): break def suitable_waiting_job(self): - available_workers = len(self.pool) + available_workers = self.pool_size - len(self.workers) for worker in self.workers: job = worker.job if job.core_usage == -1: @@ -304,11 +298,6 @@ class Server(Thread): worker.kill() except: pass - for worker in list(self.pool): - try: - worker.kill() - except: - pass def __enter__(self): return self From e835131c82d01b08a09e2713dfa39a6bff997538 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 19 Apr 2011 12:24:41 -0600 Subject: [PATCH 10/32] Halve the startup time of worker processes by delay loading cssutils, oeb.stylizer and oeb.base --- src/calibre/__init__.py | 4 ---- src/calibre/customize/builtins.py | 3 ++- src/calibre/ebooks/fb2/fb2ml.py | 14 ++++++++++---- src/calibre/ebooks/html/input.py | 3 ++- src/calibre/ebooks/html/meta.py | 2 +- src/calibre/ebooks/html/output.py | 2 +- src/calibre/ebooks/htmlz/output.py | 5 +++-- src/calibre/ebooks/oeb/base.py | 15 ++++++++------- src/calibre/ebooks/oeb/reader.py | 2 +- src/calibre/ebooks/oeb/stylizer.py | 7 ++++--- src/calibre/ebooks/oeb/transforms/filenames.py | 2 +- src/calibre/ebooks/oeb/transforms/trimmanifest.py | 3 +-- src/calibre/ebooks/pdb/ereader/writer.py | 2 +- src/calibre/ebooks/pml/output.py | 2 +- src/calibre/ebooks/pml/pmlml.py | 12 +++++++++--- src/calibre/ebooks/rb/rbml.py | 9 +++++++-- src/calibre/ebooks/rb/writer.py | 2 +- src/calibre/ebooks/rtf/rtfml.py | 9 ++++++--- src/calibre/ebooks/snb/input.py | 2 +- src/calibre/ebooks/snb/snbml.py | 5 +++-- src/calibre/ebooks/txt/output.py | 10 +++++----- src/calibre/ebooks/txt/txtml.py | 9 +++++---- src/calibre/library/catalog.py | 3 ++- 23 files changed, 75 insertions(+), 52 deletions(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 0fddb9de9d..7c9638ade1 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -33,9 +33,6 @@ if False: fcntl, win32event, isfrozen, __author__, terminal_controller winerror, win32api, isfreebsd, guess_type -import cssutils -cssutils.log.setLevel(logging.WARN) - def to_unicode(raw, encoding='utf-8', errors='strict'): if isinstance(raw, unicode): return raw @@ -679,4 +676,3 @@ main() ipshell() sys.argv = old_argv - diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 75c02c7e00..00af4e5117 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -9,7 +9,6 @@ from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \ from calibre.constants import numeric_version from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata from calibre.ebooks.metadata.opf2 import metadata_to_opf -from calibre.ebooks.oeb.base import OEB_IMAGES from calibre.utils.config import test_eight_code # To archive plugins {{{ @@ -98,6 +97,8 @@ class TXT2TXTZ(FileTypePlugin): on_import = True def _get_image_references(self, txt, base_dir): + from calibre.ebooks.oeb.base import OEB_IMAGES + images = [] # Textile diff --git a/src/calibre/ebooks/fb2/fb2ml.py b/src/calibre/ebooks/fb2/fb2ml.py index 8d1164e026..385c4a5310 100644 --- a/src/calibre/ebooks/fb2/fb2ml.py +++ b/src/calibre/ebooks/fb2/fb2ml.py @@ -18,9 +18,6 @@ from lxml import etree from calibre import prepare_string_for_xml from calibre.constants import __appname__, __version__ -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer -from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES, OPF from calibre.utils.magick import Image class FB2MLizer(object): @@ -71,7 +68,7 @@ class FB2MLizer(object): return u'' + output def clean_text(self, text): - # Condense empty paragraphs into a line break. + # Condense empty paragraphs into a line break. text = re.sub(r'(?miu)(

\s*

\s*){3,}', '', text) # Remove empty paragraphs. text = re.sub(r'(?miu)

\s*

', '', text) @@ -100,6 +97,7 @@ class FB2MLizer(object): return text def fb2_header(self): + from calibre.ebooks.oeb.base import OPF metadata = {} metadata['title'] = self.oeb_book.metadata.title[0].value metadata['appname'] = __appname__ @@ -180,6 +178,8 @@ class FB2MLizer(object): return u'' def get_cover(self): + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES + cover_href = None # Get the raster cover if it's available. @@ -213,6 +213,8 @@ class FB2MLizer(object): return u'' def get_text(self): + from calibre.ebooks.oeb.base import XHTML + from calibre.ebooks.oeb.stylizer import Stylizer text = [''] # Create main section if there are no others to create @@ -248,6 +250,8 @@ class FB2MLizer(object): ''' This function uses the self.image_hrefs dictionary mapping. It is populated by the dump_text function. ''' + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES + images = [] for item in self.oeb_book.manifest: # Don't write the image if it's not referenced in the document's text. @@ -344,6 +348,8 @@ class FB2MLizer(object): @return: List of string representing the XHTML converted to FB2 markup. ''' + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace + # Ensure what we are converting is not a string and that the fist tag is part of the XHTML namespace. if not isinstance(elem_tree.tag, basestring) or namespace(elem_tree.tag) != XHTML_NS: return [] diff --git a/src/calibre/ebooks/html/input.py b/src/calibre/ebooks/html/input.py index dd0a247a67..079e990de3 100644 --- a/src/calibre/ebooks/html/input.py +++ b/src/calibre/ebooks/html/input.py @@ -315,7 +315,8 @@ class HTMLInput(InputFormatPlugin): from calibre import guess_type from calibre.ebooks.oeb.transforms.metadata import \ meta_info_to_oeb_metadata - import cssutils + import cssutils, logging + cssutils.log.setLevel(logging.WARN) self.OEB_STYLES = OEB_STYLES oeb = create_oebbook(log, None, opts, self, encoding=opts.input_encoding, populate=False) diff --git a/src/calibre/ebooks/html/meta.py b/src/calibre/ebooks/html/meta.py index 9a088efb16..07cf9236fc 100644 --- a/src/calibre/ebooks/html/meta.py +++ b/src/calibre/ebooks/html/meta.py @@ -4,7 +4,6 @@ __copyright__ = '2010, Fabian Grassl ' __docformat__ = 'restructuredtext en' -from calibre.ebooks.oeb.base import namespace, barename, DC11_NS class EasyMeta(object): @@ -12,6 +11,7 @@ class EasyMeta(object): self.meta = meta def __iter__(self): + from calibre.ebooks.oeb.base import namespace, barename, DC11_NS meta = self.meta for item_name in meta.items: for item in meta[item_name]: diff --git a/src/calibre/ebooks/html/output.py b/src/calibre/ebooks/html/output.py index 5c984162ac..fe7b4cf274 100644 --- a/src/calibre/ebooks/html/output.py +++ b/src/calibre/ebooks/html/output.py @@ -12,7 +12,6 @@ from os.path import dirname, abspath, relpath, exists, basename from lxml import etree from templite import Templite -from calibre.ebooks.oeb.base import element from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation from calibre import CurrentDir from calibre.ptempfile import PersistentTemporaryDirectory @@ -51,6 +50,7 @@ class HTMLOutput(OutputFormatPlugin): ''' Generate table of contents ''' + from calibre.ebooks.oeb.base import element with CurrentDir(output_dir): def build_node(current_node, parent=None): if parent is None: diff --git a/src/calibre/ebooks/htmlz/output.py b/src/calibre/ebooks/htmlz/output.py index 03fe12c89e..6d2ad54a12 100644 --- a/src/calibre/ebooks/htmlz/output.py +++ b/src/calibre/ebooks/htmlz/output.py @@ -12,7 +12,6 @@ from lxml import etree from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation -from calibre.ebooks.oeb.base import OEB_IMAGES, SVG_MIME from calibre.ptempfile import TemporaryDirectory from calibre.utils.zipfile import ZipFile @@ -42,6 +41,8 @@ class HTMLZOutput(OutputFormatPlugin): ]) def convert(self, oeb_book, output_path, input_plugin, opts, log): + from calibre.ebooks.oeb.base import OEB_IMAGES, SVG_MIME + # HTML if opts.htmlz_css_type == 'inline': from calibre.ebooks.htmlz.oeb2html import OEB2HTMLInlineCSSizer @@ -72,7 +73,7 @@ class HTMLZOutput(OutputFormatPlugin): for item in oeb_book.manifest: if item.media_type in OEB_IMAGES and item.href in images: if item.media_type == SVG_MIME: - data = unicode(etree.tostring(item.data, encoding=unicode)) + data = unicode(etree.tostring(item.data, encoding=unicode)) else: data = item.data fname = os.path.join(tdir, 'images', images[item.href]) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 58083f807f..ce75c97d78 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -15,11 +15,7 @@ from urlparse import urldefrag, urlparse, urlunparse, urljoin from urllib import unquote as urlunquote from lxml import etree, html -from cssutils import CSSParser, parseString, parseStyle, replaceUrls -from cssutils.css import CSSRule - -import calibre -from calibre.constants import filesystem_encoding +from calibre.constants import filesystem_encoding, __version__ from calibre.translations.dynamic import translate from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.oeb.entitydefs import ENTITYDEFS @@ -179,6 +175,9 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False): If the ``link_repl_func`` returns None, the attribute or tag text will be removed completely. ''' + from cssutils import parseString, parseStyle, replaceUrls, log + log.setLevel(logging.WARN) + if resolve_base_href: resolve_base_href(root) for el, attrib, link, pos in iterlinks(root, find_links_in_css=False): @@ -1075,7 +1074,9 @@ class Manifest(object): def _parse_css(self, data): - + from cssutils.css import CSSRule + from cssutils import CSSParser, log + log.setLevel(logging.WARN) def get_style_rules_from_import(import_rule): ans = [] if not import_rule.styleSheet: @@ -2011,7 +2012,7 @@ class OEBBook(object): name='dtb:uid', content=unicode(self.uid)) etree.SubElement(head, NCX('meta'), name='dtb:depth', content=str(self.toc.depth())) - generator = ''.join(['calibre (', calibre.__version__, ')']) + generator = ''.join(['calibre (', __version__, ')']) etree.SubElement(head, NCX('meta'), name='dtb:generator', content=generator) etree.SubElement(head, NCX('meta'), diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index ebc2f30d00..1c42a5a242 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -14,7 +14,6 @@ from mimetypes import guess_type from collections import defaultdict from lxml import etree -import cssutils from calibre.ebooks.oeb.base import OPF1_NS, OPF2_NS, OPF2_NSMAP, DC11_NS, \ DC_NSES, OPF, xml2text @@ -172,6 +171,7 @@ class OEBReader(object): return bad def _manifest_add_missing(self, invalid): + import cssutils manifest = self.oeb.manifest known = set(manifest.hrefs) unchecked = set(manifest.values()) diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 634f7f5fce..39ab41eede 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -12,17 +12,18 @@ import os, itertools, re, logging, copy, unicodedata from weakref import WeakKeyDictionary from xml.dom import SyntaxErr as CSSSyntaxError import cssutils -from cssutils.css import CSSStyleRule, CSSPageRule, CSSStyleDeclaration, \ - CSSValueList, CSSFontFaceRule, cssproperties +from cssutils.css import (CSSStyleRule, CSSPageRule, CSSStyleDeclaration, + CSSValueList, CSSFontFaceRule, cssproperties) from cssutils import profile as cssprofiles from lxml import etree from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError - from calibre import force_unicode from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize from calibre.ebooks.oeb.profile import PROFILES +cssutils.log.setLevel(logging.WARN) + _html_css_stylesheet = None def html_css_stylesheet(): diff --git a/src/calibre/ebooks/oeb/transforms/filenames.py b/src/calibre/ebooks/oeb/transforms/filenames.py index bad75b9a6f..c3c7f091c3 100644 --- a/src/calibre/ebooks/oeb/transforms/filenames.py +++ b/src/calibre/ebooks/oeb/transforms/filenames.py @@ -9,7 +9,6 @@ import posixpath from urlparse import urldefrag, urlparse from lxml import etree -import cssutils from calibre.ebooks.oeb.base import rewrite_links, urlnormalize @@ -25,6 +24,7 @@ class RenameFiles(object): # {{{ self.renamed_items_map = renamed_items_map def __call__(self, oeb, opts): + import cssutils self.log = oeb.logger self.opts = opts self.oeb = oeb diff --git a/src/calibre/ebooks/oeb/transforms/trimmanifest.py b/src/calibre/ebooks/oeb/transforms/trimmanifest.py index 0baacfd1f9..95501dbb9b 100644 --- a/src/calibre/ebooks/oeb/transforms/trimmanifest.py +++ b/src/calibre/ebooks/oeb/transforms/trimmanifest.py @@ -8,8 +8,6 @@ __copyright__ = '2008, Marshall T. Vandegrift ' from urlparse import urldefrag -import cssutils - from calibre.ebooks.oeb.base import CSS_MIME, OEB_DOCS from calibre.ebooks.oeb.base import urlnormalize, iterlinks @@ -23,6 +21,7 @@ class ManifestTrimmer(object): return cls() def __call__(self, oeb, context): + import cssutils oeb.logger.info('Trimming unused files from manifest...') self.opts = context used = set() diff --git a/src/calibre/ebooks/pdb/ereader/writer.py b/src/calibre/ebooks/pdb/ereader/writer.py index 4fbd343a6b..eb023c594b 100644 --- a/src/calibre/ebooks/pdb/ereader/writer.py +++ b/src/calibre/ebooks/pdb/ereader/writer.py @@ -21,7 +21,6 @@ except ImportError: import cStringIO from calibre.ebooks.pdb.formatwriter import FormatWriter -from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES from calibre.ebooks.pdb.header import PdbHeaderBuilder from calibre.ebooks.pml.pmlml import PMLMLizer @@ -135,6 +134,7 @@ class Writer(FormatWriter): 62-...: Raw image data in 8 bit PNG format. ''' images = [] + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES for item in manifest: if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys(): diff --git a/src/calibre/ebooks/pml/output.py b/src/calibre/ebooks/pml/output.py index 9d2ddc6ca6..63d8a8b220 100644 --- a/src/calibre/ebooks/pml/output.py +++ b/src/calibre/ebooks/pml/output.py @@ -18,7 +18,6 @@ from calibre.customize.conversion import OutputFormatPlugin from calibre.customize.conversion import OptionRecommendation from calibre.ptempfile import TemporaryDirectory from calibre.utils.zipfile import ZipFile -from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES from calibre.ebooks.pml.pmlml import PMLMLizer class PMLOutput(OutputFormatPlugin): @@ -60,6 +59,7 @@ class PMLOutput(OutputFormatPlugin): pmlz.add_dir(tdir) def write_images(self, manifest, image_hrefs, out_dir, opts): + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES for item in manifest: if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys(): if opts.full_image_depth: diff --git a/src/calibre/ebooks/pml/pmlml.py b/src/calibre/ebooks/pml/pmlml.py index 779e75d713..b04aaacaec 100644 --- a/src/calibre/ebooks/pml/pmlml.py +++ b/src/calibre/ebooks/pml/pmlml.py @@ -12,8 +12,6 @@ import re from lxml import etree -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer from calibre.ebooks.pdb.ereader import image_name from calibre.ebooks.pml import unipmlcode @@ -110,6 +108,9 @@ class PMLMLizer(object): return output def get_cover_page(self): + from calibre.ebooks.oeb.stylizer import Stylizer + from calibre.ebooks.oeb.base import XHTML + output = u'' if 'cover' in self.oeb_book.guide: output += '\\m="cover.png"\n' @@ -125,6 +126,9 @@ class PMLMLizer(object): return output def get_text(self): + from calibre.ebooks.oeb.stylizer import Stylizer + from calibre.ebooks.oeb.base import XHTML + text = [u''] for item in self.oeb_book.spine: self.log.debug('Converting %s to PML markup...' % item.href) @@ -180,7 +184,7 @@ class PMLMLizer(object): links = set(re.findall(r'(?<=\\q="#).+?(?=")', text)) for unused in anchors.difference(links): text = text.replace('\\Q="%s"' % unused, '') - + # Remove \Cn tags that are within \x and \Xn tags text = re.sub(ur'(?msu)(?P\\(x|X[0-4]))(?P.*?)(?P\\C[0-4]\s*=\s*"[^"]*")(?P.*?)(?P=t)', '\g\g\g\g', text) @@ -214,6 +218,8 @@ class PMLMLizer(object): return text def dump_text(self, elem, stylizer, page, tag_stack=[]): + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace + if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: return [] diff --git a/src/calibre/ebooks/rb/rbml.py b/src/calibre/ebooks/rb/rbml.py index 50153d7d4d..8cf63e334c 100644 --- a/src/calibre/ebooks/rb/rbml.py +++ b/src/calibre/ebooks/rb/rbml.py @@ -11,8 +11,6 @@ Transform OEB content into RB compatible markup. import re from calibre import prepare_string_for_xml -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer from calibre.ebooks.rb import unique_name TAGS = [ @@ -81,6 +79,8 @@ class RBMLizer(object): return output def get_cover_page(self): + from calibre.ebooks.oeb.stylizer import Stylizer + from calibre.ebooks.oeb.base import XHTML output = u'' if 'cover' in self.oeb_book.guide: if self.name_map.get(self.oeb_book.guide['cover'].href, None): @@ -109,6 +109,9 @@ class RBMLizer(object): return ''.join(toc) def get_text(self): + from calibre.ebooks.oeb.stylizer import Stylizer + from calibre.ebooks.oeb.base import XHTML + output = [u''] for item in self.oeb_book.spine: self.log.debug('Converting %s to RocketBook HTML...' % item.href) @@ -137,6 +140,8 @@ class RBMLizer(object): return text def dump_text(self, elem, stylizer, page, tag_stack=[]): + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace + if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: return [u''] diff --git a/src/calibre/ebooks/rb/writer.py b/src/calibre/ebooks/rb/writer.py index c8908ee95f..f71b103fbd 100644 --- a/src/calibre/ebooks/rb/writer.py +++ b/src/calibre/ebooks/rb/writer.py @@ -18,7 +18,6 @@ import cStringIO from calibre.ebooks.rb.rbml import RBMLizer from calibre.ebooks.rb import HEADER from calibre.ebooks.rb import unique_name -from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES from calibre.constants import __appname__, __version__ TEXT_RECORD_SIZE = 4096 @@ -111,6 +110,7 @@ class RBWriter(object): return (size, pages) def _images(self, manifest): + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES images = [] used_names = [] diff --git a/src/calibre/ebooks/rtf/rtfml.py b/src/calibre/ebooks/rtf/rtfml.py index f739207018..97fa175d1a 100644 --- a/src/calibre/ebooks/rtf/rtfml.py +++ b/src/calibre/ebooks/rtf/rtfml.py @@ -14,9 +14,6 @@ import cStringIO from lxml import etree -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace, \ - OEB_RASTER_IMAGES -from calibre.ebooks.oeb.stylizer import Stylizer from calibre.ebooks.metadata import authors_to_string from calibre.utils.filenames import ascii_text from calibre.utils.magick.draw import save_cover_data_to, identify_data @@ -100,6 +97,8 @@ class RTFMLizer(object): return self.mlize_spine() def mlize_spine(self): + from calibre.ebooks.oeb.base import XHTML + from calibre.ebooks.oeb.stylizer import Stylizer output = self.header() if 'titlepage' in self.oeb_book.guide: href = self.oeb_book.guide['titlepage'].href @@ -154,6 +153,8 @@ class RTFMLizer(object): return ' }' def insert_images(self, text): + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES + for item in self.oeb_book.manifest: if item.media_type in OEB_RASTER_IMAGES: src = os.path.basename(item.href) @@ -201,6 +202,8 @@ class RTFMLizer(object): return text def dump_text(self, elem, stylizer, tag_stack=[]): + from calibre.ebooks.oeb.base import XHTML_NS, namespace, barename + if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: p = elem.getparent() diff --git a/src/calibre/ebooks/snb/input.py b/src/calibre/ebooks/snb/input.py index 100ac1447f..13b1ca45f9 100755 --- a/src/calibre/ebooks/snb/input.py +++ b/src/calibre/ebooks/snb/input.py @@ -7,7 +7,6 @@ __docformat__ = 'restructuredtext en' import os, uuid from calibre.customize.conversion import InputFormatPlugin -from calibre.ebooks.oeb.base import DirContainer from calibre.ebooks.snb.snbfile import SNBFile from calibre.ptempfile import TemporaryDirectory from calibre.utils.filenames import ascii_filename @@ -30,6 +29,7 @@ class SNBInput(InputFormatPlugin): def convert(self, stream, options, file_ext, log, accelerators): + from calibre.ebooks.oeb.base import DirContainer log.debug("Parsing SNB file...") snbFile = SNBFile() try: diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index 078e7ebe76..a501de1ff0 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -13,8 +13,6 @@ import re from lxml import etree -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer def ProcessFileName(fileName): # Flat the path @@ -81,6 +79,8 @@ class SNBMLizer(object): body.append(entity) def mlize(self): + from calibre.ebooks.oeb.base import XHTML + from calibre.ebooks.oeb.stylizer import Stylizer output = [ u'' ] stylizer = Stylizer(self.item.data, self.item.href, self.oeb_book, self.opts, self.opts.output_profile) content = unicode(etree.tostring(self.item.data.find(XHTML('body')), encoding=unicode)) @@ -208,6 +208,7 @@ class SNBMLizer(object): return text def dump_text(self, subitems, elem, stylizer, end='', pre=False, li = ''): + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: diff --git a/src/calibre/ebooks/txt/output.py b/src/calibre/ebooks/txt/output.py index 4e54a97b45..ac63690996 100644 --- a/src/calibre/ebooks/txt/output.py +++ b/src/calibre/ebooks/txt/output.py @@ -11,7 +11,6 @@ from lxml import etree from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation -from calibre.ebooks.oeb.base import OEB_IMAGES from calibre.ebooks.txt.txtml import TXTMLizer from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines from calibre.ptempfile import TemporaryDirectory, TemporaryFile @@ -103,12 +102,13 @@ class TXTOutput(OutputFormatPlugin): class TXTZOutput(TXTOutput): - + name = 'TXTZ Output' author = 'John Schember' file_type = 'txtz' def convert(self, oeb_book, output_path, input_plugin, opts, log): + from calibre.ebooks.oeb.base import OEB_IMAGES with TemporaryDirectory('_txtz_output') as tdir: # TXT with TemporaryFile('index.txt') as tf: @@ -123,10 +123,10 @@ class TXTZOutput(TXTOutput): os.makedirs(path) with open(os.path.join(tdir, item.href), 'wb') as imgf: imgf.write(item.data) - + # Metadata - with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf: + with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf: mdataf.write(etree.tostring(oeb_book.metadata.to_opf1())) - + txtz = ZipFile(output_path, 'w') txtz.add_dir(tdir) diff --git a/src/calibre/ebooks/txt/txtml.py b/src/calibre/ebooks/txt/txtml.py index fa7bfbb380..2320fbbbc7 100644 --- a/src/calibre/ebooks/txt/txtml.py +++ b/src/calibre/ebooks/txt/txtml.py @@ -12,8 +12,6 @@ import re from lxml import etree -from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace -from calibre.ebooks.oeb.stylizer import Stylizer BLOCK_TAGS = [ 'div', @@ -58,12 +56,14 @@ class TXTMLizer(object): self.toc_titles = [] self.toc_ids = [] self.last_was_heading = False - + self.create_flat_toc(self.oeb_book.toc) return self.mlize_spine() def mlize_spine(self): + from calibre.ebooks.oeb.base import XHTML + from calibre.ebooks.oeb.stylizer import Stylizer output = [u''] output.append(self.get_toc()) for item in self.oeb_book.spine: @@ -139,7 +139,7 @@ class TXTMLizer(object): # when remove paragraph spacing is enabled. text = re.sub('(?imu)^[ ]+', '', text) text = re.sub('(?imu)[ ]+$', '', text) - + # Remove empty space and newlines at the beginning of the document. text = re.sub(r'(?u)^[ \n]+', '', text) @@ -185,6 +185,7 @@ class TXTMLizer(object): @stylizer: The style information attached to the element. @page: OEB page used to determine absolute urls. ''' + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index ffa08eaed2..717e8e2c6b 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -15,7 +15,6 @@ from calibre.customize import CatalogPlugin from calibre.customize.conversion import OptionRecommendation, DummyReporter from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString from calibre.ebooks.chardet import substitute_entites -from calibre.ebooks.oeb.base import XHTML_NS from calibre.ptempfile import PersistentTemporaryDirectory from calibre.utils.config import config_dir from calibre.utils.date import format_date, isoformat, is_date_undefined, now as nowf @@ -4322,6 +4321,8 @@ Author '{0}': ''' Generate description header from template ''' + from calibre.ebooks.oeb.base import XHTML_NS + def generate_html(): args = dict( author=author, From f76307f9d54e6253ca4fc06be6777f9ac2bc3881 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 19 Apr 2011 12:33:38 -0600 Subject: [PATCH 11/32] Speedup import of gui2.__init__ --- src/calibre/gui2/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 3ad8a81ffa..de066359ed 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -4,19 +4,17 @@ __copyright__ = '2008, Kovid Goyal ' import os, sys, Queue, threading from threading import RLock from urllib import unquote - -from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ - QByteArray, QTranslator, QCoreApplication, QThread, \ - QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ - QFileDialog, QFileIconProvider, \ - QIcon, QApplication, QDialog, QUrl, QFont +from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, + QByteArray, QTranslator, QCoreApplication, QThread, + QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, + QFileDialog, QFileIconProvider, + QIcon, QApplication, QDialog, QUrl, QFont) ORG_NAME = 'KovidsBrain' APP_UID = 'libprs500' from calibre.constants import islinux, iswindows, isfreebsd, isfrozen, isosx from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig from calibre.utils.localization import set_qt_translator -from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats from calibre.ebooks.metadata import MetaInformation from calibre.utils.date import UNDEFINED_DATE @@ -332,6 +330,7 @@ class GetMetadata(QObject): id, args, kwargs) def _from_formats(self, id, args, kwargs): + from calibre.ebooks.metadata.meta import metadata_from_formats try: mi = metadata_from_formats(*args, **kwargs) except: @@ -339,6 +338,7 @@ class GetMetadata(QObject): self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi) def _get_metadata(self, id, args, kwargs): + from calibre.ebooks.metadata.meta import get_metadata try: mi = get_metadata(*args, **kwargs) except: @@ -740,3 +740,4 @@ def build_forms(srcdir, info=None): _df = os.environ.get('CALIBRE_DEVELOP_FROM', None) if _df and os.path.exists(_df): build_forms(_df) + From 1206cb3304f8482e7809cec0132f96ab45f4ef5a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 19 Apr 2011 12:43:09 -0600 Subject: [PATCH 12/32] Delay load the metadata read/write subsystem --- src/calibre/gui2/device.py | 2 +- src/calibre/gui2/dialogs/metadata_bulk.py | 2 +- src/calibre/gui2/dialogs/metadata_single.py | 2 +- src/calibre/gui2/library/models.py | 3 ++- src/calibre/gui2/widgets.py | 2 +- src/calibre/library/database2.py | 12 +++++++++--- src/calibre/library/save_to_disk.py | 2 +- 7 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index db3a43e47d..49542abdc1 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -25,7 +25,6 @@ from calibre.devices.errors import FreeSpaceError from calibre.devices.apple.driver import ITUNES_ASYNC from calibre.devices.folder_device.driver import FOLDER_DEVICE from calibre.devices.bambook.driver import BAMBOOK, BAMBOOKWifi -from calibre.ebooks.metadata.meta import set_metadata from calibre.constants import DEBUG from calibre.utils.config import prefs, tweaks from calibre.utils.magick.draw import thumbnail @@ -334,6 +333,7 @@ class DeviceManager(Thread): # {{{ def _upload_books(self, files, names, on_card=None, metadata=None, plugboards=None): '''Upload books to device: ''' + from calibre.ebooks.metadata.meta import set_metadata if hasattr(self.connected_device, 'set_plugboards') and \ callable(self.connected_device.set_plugboards): self.connected_device.set_plugboards(plugboards, find_plugboard) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 8a97183ffe..66cf55a9b2 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -13,7 +13,6 @@ from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.ebooks.metadata import string_to_authors, authors_to_string, title_sort from calibre.ebooks.metadata.book.base import composite_formatter -from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE, \ gprefs, question_dialog @@ -26,6 +25,7 @@ from calibre.utils.magick.draw import identify_data from calibre.utils.date import qt_to_dt def get_cover_data(path): # {{{ + from calibre.ebooks.metadata.meta import get_metadata old = prefs['read_file_metadata'] if not old: prefs['read_file_metadata'] = True diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index f6b7b94453..4776562c29 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -25,7 +25,6 @@ from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.metadata import string_to_authors, \ authors_to_string, check_isbn, title_sort from calibre.ebooks.metadata.covers import download_cover -from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata import MetaInformation from calibre.utils.config import prefs, tweaks from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp @@ -353,6 +352,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.formats_changed = True def get_selected_format_metadata(self): + from calibre.ebooks.metadata.meta import get_metadata old = prefs['read_file_metadata'] if not old: prefs['read_file_metadata'] = True diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 8d89ec76ed..0bd3f2133a 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -18,7 +18,6 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import tweaks, prefs from calibre.utils.date import dt_factory, qt_to_dt, isoformat from calibre.utils.icu import sort_key -from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.utils.search_query_parser import SearchQueryParser from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ REGEXP_MATCH, MetadataBackup, force_to_bool @@ -478,6 +477,7 @@ class BooksModel(QAbstractTableModel): # {{{ def get_preferred_formats_from_ids(self, ids, formats, set_metadata=False, specific_format=None, exclude_auto=False, mode='r+b'): + from calibre.ebooks.metadata.meta import set_metadata as _set_metadata ans = [] need_auto = [] if specific_format is not None: @@ -526,6 +526,7 @@ class BooksModel(QAbstractTableModel): # {{{ def get_preferred_formats(self, rows, formats, paths=False, set_metadata=False, specific_format=None, exclude_auto=False): + from calibre.ebooks.metadata.meta import set_metadata as _set_metadata ans = [] need_auto = [] if specific_format is not None: diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index ea0d2570e5..a7ecdf7b88 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -19,7 +19,6 @@ from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs from calibre.gui2.filename_pattern_ui import Ui_Form from calibre import fit_image from calibre.ebooks import BOOK_EXTENSIONS -from calibre.ebooks.metadata.meta import metadata_from_filename from calibre.utils.config import prefs, XMLConfig, tweaks from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \ @@ -95,6 +94,7 @@ class FilenamePattern(QWidget, Ui_Form): self.re.setCurrentIndex(0) def do_test(self): + from calibre.ebooks.metadata.meta import metadata_from_filename try: pat = self.pattern() except Exception as err: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index bc0a8235e4..d7f6c22925 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -15,7 +15,8 @@ from math import ceil from PyQt4.QtGui import QImage from calibre import prints -from calibre.ebooks.metadata import title_sort, author_to_author_sort +from calibre.ebooks.metadata import (title_sort, author_to_author_sort, + string_to_authors, authors_to_string) from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.library.database import LibraryDatabase from calibre.library.field_metadata import FieldMetadata, TagsIcons @@ -24,9 +25,7 @@ from calibre.library.caches import ResultCache from calibre.library.custom_columns import CustomColumns from calibre.library.sqlite import connect, IntegrityError from calibre.library.prefs import DBPrefs -from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.ebooks.metadata.book.base import Metadata -from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding from calibre.ptempfile import PersistentTemporaryFile from calibre.customize.ui import run_plugins_on_import @@ -2729,6 +2728,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.set_identifier(id_, 'isbn', isbn, notify=notify, commit=commit) def add_catalog(self, path, title): + from calibre.ebooks.metadata.meta import get_metadata + format = os.path.splitext(path)[1][1:].lower() with lopen(path, 'rb') as stream: matches = self.data.get_matches('title', '='+title) @@ -2764,6 +2765,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def add_news(self, path, arg): + from calibre.ebooks.metadata.meta import get_metadata + format = os.path.splitext(path)[1][1:].lower() stream = path if hasattr(path, 'read') else lopen(path, 'rb') stream.seek(0) @@ -3157,6 +3160,8 @@ books_series_link feeds yield formats def import_book_directory_multiple(self, dirpath, callback=None): + from calibre.ebooks.metadata.meta import metadata_from_formats + duplicates = [] for formats in self.find_books_in_directory(dirpath, False): mi = metadata_from_formats(formats) @@ -3172,6 +3177,7 @@ books_series_link feeds return duplicates def import_book_directory(self, dirpath, callback=None): + from calibre.ebooks.metadata.meta import metadata_from_formats dirpath = os.path.abspath(dirpath) formats = self.find_books_in_directory(dirpath, True) formats = list(formats)[0] diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index 42e6c8b156..f7f5559412 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -14,7 +14,6 @@ from calibre.utils.formatter import TemplateFormatter from calibre.utils.filenames import shorten_components_to, supports_long_names, \ ascii_filename from calibre.ebooks.metadata.opf2 import metadata_to_opf -from calibre.ebooks.metadata.meta import set_metadata from calibre.constants import preferred_encoding from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import title_sort @@ -251,6 +250,7 @@ def save_book_to_disk(id_, db, root, opts, length): def do_save_book_to_disk(id_, mi, cover, plugboards, format_map, root, opts, length): + from calibre.ebooks.metadata.meta import set_metadata available_formats = [x.lower().strip() for x in format_map.keys()] if opts.formats == 'all': asked_formats = available_formats From e776b5b1f6ddd107dd94272c3067dcc426407313 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 19 Apr 2011 14:18:28 -0600 Subject: [PATCH 13/32] ... --- src/calibre/__init__.py | 5 +++-- src/calibre/ebooks/conversion/plumber.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 7c9638ade1..0bf48a8a97 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -4,8 +4,7 @@ __copyright__ = '2008, Kovid Goyal ' __docformat__ = 'restructuredtext en' import uuid, sys, os, re, logging, time, random, \ - __builtin__, warnings, multiprocessing -from contextlib import closing + __builtin__, warnings from urllib import getproxies from urllib2 import unquote as urllib2_unquote __builtin__.__dict__['dynamic_property'] = lambda(func): func(None) @@ -383,6 +382,7 @@ class StreamReadWrapper(object): def detect_ncpus(): """Detects the number of effective CPUs in the system""" + import multiprocessing ans = -1 try: ans = multiprocessing.cpu_count() @@ -547,6 +547,7 @@ def get_download_filename(url, cookie_file=None): ''' Get a local filename for a URL using the content disposition header ''' + from contextlib import closing filename = '' br = browser() diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index b26befe075..8706d6eb2b 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -875,6 +875,8 @@ OptionRecommendation(name='sr3_replace', if self.opts.verbose: self.log.filter_level = self.log.DEBUG self.flush() + import cssutils, logging + cssutils.log.setLevel(logging.WARN) if self.opts.debug_pipeline is not None: self.opts.verbose = max(self.opts.verbose, 4) From 3f71ad9420ab3bf359bc667743b6935affdaf00a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 19 Apr 2011 14:54:41 -0600 Subject: [PATCH 14/32] Fix Newsweek --- recipes/newsweek.recipe | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/recipes/newsweek.recipe b/recipes/newsweek.recipe index 73837c1872..97abd69aac 100644 --- a/recipes/newsweek.recipe +++ b/recipes/newsweek.recipe @@ -1,4 +1,3 @@ -import string from calibre.web.feeds.news import BasicNewsRecipe class Newsweek(BasicNewsRecipe): @@ -11,7 +10,6 @@ class Newsweek(BasicNewsRecipe): no_stylesheets = True BASE_URL = 'http://www.newsweek.com' - INDEX = BASE_URL+'/topics.html' keep_only_tags = dict(name='article', attrs={'class':'article-text'}) remove_tags = [dict(attrs={'data-dartad':True})] @@ -23,11 +21,14 @@ class Newsweek(BasicNewsRecipe): return soup def newsweek_sections(self): - soup = self.index_to_soup(self.INDEX) - for a in soup.findAll('a', title='Primary tag', href=True): - yield (string.capitalize(self.tag_to_string(a)), - self.BASE_URL+a['href']) - + return [ + ('Nation', 'http://www.newsweek.com/tag/nation.html'), + ('Society', 'http://www.newsweek.com/tag/society.html'), + ('Culture', 'http://www.newsweek.com/tag/culture.html'), + ('World', 'http://www.newsweek.com/tag/world.html'), + ('Politics', 'http://www.newsweek.com/tag/politics.html'), + ('Business', 'http://www.newsweek.com/tag/business.html'), + ] def newsweek_parse_section_page(self, soup): for article in soup.findAll('article', about=True, From 2b21ea3d9b8e571885607351cf7148df6f4eea41 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 19 Apr 2011 18:06:56 -0600 Subject: [PATCH 15/32] Der Spiegel by Nikolas Mangold --- recipes/der_spiegel.recipe | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 recipes/der_spiegel.recipe diff --git a/recipes/der_spiegel.recipe b/recipes/der_spiegel.recipe new file mode 100644 index 0000000000..1e94785233 --- /dev/null +++ b/recipes/der_spiegel.recipe @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = '2011, Nikolas Mangold ' +''' +spiegel.de +''' +from calibre.web.feeds.news import BasicNewsRecipe +from calibre import strftime +from calibre import re + +class DerSpiegel(BasicNewsRecipe): + title = 'Der Spiegel' + __author__ = 'Nikolas Mangold' + description = 'Der Spiegel, Printed Edition. Access to paid content.' + publisher = 'SPIEGEL-VERLAG RUDOLF AUGSTEIN GMBH & CO. KG' + category = 'news, politics, Germany' + no_stylesheets = True + encoding = 'cp1252' + needs_subscription = True + remove_empty_feeds = True + delay = 1 + PREFIX = 'http://m.spiegel.de' + INDEX = PREFIX + '/spiegel/print/epaper/index-heftaktuell.html' + use_embedded_content = False + masthead_url = 'http://upload.wikimedia.org/wikipedia/en/thumb/1/17/Der_Spiegel_logo.svg/200px-Der_Spiegel_logo.svg.png' + language = 'de' + publication_type = 'magazine' + extra_css = ' body{font-family: Arial,Helvetica,sans-serif} ' + timefmt = '[%W/%Y]' + empty_articles = ['Titelbild'] + preprocess_regexps = [ + (re.compile(r'

', re.DOTALL|re.IGNORECASE), lambda match: '
'), + ] + + def get_browser(self): + def has_login_name(form): + try: + form.find_control(name="f.loginName") + except: + return False + else: + return True + + br = BasicNewsRecipe.get_browser() + if self.username is not None and self.password is not None: + br.open(self.PREFIX + '/meinspiegel/login.html') + br.select_form(predicate=has_login_name) + br['f.loginName' ] = self.username + br['f.password'] = self.password + br.submit() + return br + + remove_tags_before = dict(attrs={'class':'spArticleContent'}) + remove_tags_after = dict(attrs={'class':'spArticleCredit'}) + + def parse_index(self): + soup = self.index_to_soup(self.INDEX) + + cover = soup.find('img', width=248) + if cover is not None: + self.cover_url = cover['src'] + + index = soup.find('dl') + + feeds = [] + for section in index.findAll('dt'): + section_title = self.tag_to_string(section).strip() + self.log('Found section ', section_title) + + articles = [] + for article in section.findNextSiblings(['dd','dt']): + if article.name == 'dt': + break + link = article.find('a') + title = self.tag_to_string(link).strip() + if title in self.empty_articles: + continue + self.log('Found article ', title) + url = self.PREFIX + link['href'] + articles.append({'title' : title, 'date' : strftime(self.timefmt), 'url' : url}) + feeds.append((section_title,articles)) + return feeds; From cbe800e423fa05461556821114621b8720976080 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 20 Apr 2011 09:43:14 -0600 Subject: [PATCH 16/32] Enable Qt SSL in the calibre windows build and a couple of fixes for the overdrive plugin --- setup/installer/windows/freeze.py | 5 +- setup/installer/windows/notes.rst | 15 +++++- .../ebooks/metadata/sources/overdrive.py | 10 ++-- src/calibre/gui2/store/mobileread_plugin.py | 50 +++++++++---------- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/setup/installer/windows/freeze.py b/setup/installer/windows/freeze.py index cf4dcd5f9d..f666427598 100644 --- a/setup/installer/windows/freeze.py +++ b/setup/installer/windows/freeze.py @@ -13,7 +13,8 @@ from setup import Command, modules, functions, basenames, __version__, \ from setup.build_environment import msvc, MT, RC from setup.installer.windows.wix import WixMixIn -QT_DIR = 'Q:\\Qt\\4.7.1' +OPENSSL_DIR = r'Q:\openssl' +QT_DIR = 'Q:\\Qt\\4.7.2' QT_DLLS = ['Core', 'Gui', 'Network', 'Svg', 'WebKit', 'Xml', 'XmlPatterns'] LIBUSB_DIR = 'C:\\libusb' LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll' @@ -108,6 +109,8 @@ class Win32Freeze(Command, WixMixIn): self.dll_dir = self.j(self.base, 'DLLs') shutil.copytree(r'C:\Python%s\DLLs'%self.py_ver, self.dll_dir, ignore=shutil.ignore_patterns('msvc*.dll', 'Microsoft.*')) + for x in glob.glob(self.j(OPENSSL_DIR, 'bin', '*.dll')): + shutil.copy2(x, self.dll_dir) for x in QT_DLLS: x += '4.dll' if not x.startswith('phonon'): x = 'Qt'+x diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index 5dfd956ce2..ce6ca650a4 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -53,12 +53,25 @@ SQLite Put sqlite3*.h from the sqlite windows amlgamation in ~/sw/include +OpenSSL +-------- + +First install ActiveState Perl if you dont already have perl in windows +Download and untar the openssl tarball, follow the instructions in INSTALL.W32 (use no-asm) +to install use prefix q:\openssl + +perl Configure VC-WIN32 no-asm enable-static-engine --prefix=Q:/openssl +ms\do_ms.bat +nmake -f ms\ntdll.mak +nmake -f ms\ntdll.mak test +nmake -f ms\ntdll.mak install + Qt -------- Extract Qt sourcecode to C:\Qt\4.x.x. Run configure and make:: - configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license -nomake examples -nomake demos -nomake docs && nmake + configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license -nomake examples -nomake demos -nomake docs -openssl -I Q:\openssl\include -L Q:\openssl\lib && nmake SIP ----- diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py index 26c90f08fe..5f70802314 100755 --- a/src/calibre/ebooks/metadata/sources/overdrive.py +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en' ''' Fetch metadata using Overdrive Content Reserve ''' -import re, random, mechanize, copy +import re, random, mechanize, copy, json from threading import RLock from Queue import Queue, Empty @@ -43,7 +43,7 @@ class OverDrive(Source): def __init__(self, *args, **kwargs): Source.__init__(self, *args, **kwargs) - self.prefs.defaults['ignore_fields'] =['tags', 'pubdate', 'comments', 'identifier:isbn', 'language'] + self.prefs.defaults['ignore_fields'] =['tags', 'pubdate', 'comments', 'identifier:isbn'] def identify(self, log, result_queue, abort, title=None, authors=None, # {{{ identifiers={}, timeout=30): @@ -228,7 +228,7 @@ class OverDrive(Source): def sort_ovrdrv_results(self, raw, title=None, title_tokens=None, author=None, author_tokens=None, ovrdrv_id=None): close_matches = [] raw = re.sub('.*?\[\[(?P.*?)\]\].*', '[[\g]]', raw) - results = eval(raw) + results = json.loads(raw) #print results # The search results are either from a keyword search or a multi-format list from a single ID, # sort through the results for closest match/format @@ -392,7 +392,9 @@ class OverDrive(Source): from calibre.utils.date import parse_date mi.pubdate = parse_date(pub_date[0].strip()) if lang: - mi.language = lang[0].strip() + lang = lang[0].strip().lower() + mi.language = {'english':'en', 'french':'fr', 'german':'de', + 'spanish':'es'}.get(lang, None) if ebook_isbn: #print "ebook isbn is "+str(ebook_isbn[0]) diff --git a/src/calibre/gui2/store/mobileread_plugin.py b/src/calibre/gui2/store/mobileread_plugin.py index 49c265d7fe..b65748ac57 100644 --- a/src/calibre/gui2/store/mobileread_plugin.py +++ b/src/calibre/gui2/store/mobileread_plugin.py @@ -28,15 +28,15 @@ from calibre.utils.config import DynamicConfig from calibre.utils.icu import sort_key class MobileReadStore(BasicStoreConfig, StorePlugin): - + def genesis(self): self.config = DynamicConfig('store_' + self.name) self.rlock = RLock() - + def open(self, parent=None, detail_item=None, external=False): settings = self.get_settings() url = 'http://www.mobileread.com/' - + if external or settings.get(self.name + '_open_external', False): open_url(QUrl(detail_item if detail_item else url)) else: @@ -71,7 +71,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): ratio += s.ratio() if ratio > 0: matches.append((ratio, x)) - + # Move the best scorers to head of list. matches = heapq.nlargest(max_results, matches) for score, book in matches: @@ -81,21 +81,21 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): def update_book_list(self, timeout=10): with self.rlock: url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html' - + last_download = self.config.get(self.name + '_last_download', None) # Don't update the book list if our cache is less than one week old. if last_download and (time.time() - last_download) < 604800: return - + # Download the book list HTML file from MobileRead. br = browser() raw_data = None with closing(br.open(url, timeout=timeout)) as f: raw_data = f.read() - + if not raw_data: return - + # Turn books listed in the HTML file into BookRef's. books = [] try: @@ -105,7 +105,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): book.detail_item = ''.join(book_data.xpath('.//a/@href')) book.format = ''.join(book_data.xpath('.//i/text()')) book.format = book.format.strip() - + text = ''.join(book_data.xpath('.//a/text()')) if ':' in text: book.author, q, text = text.partition(':') @@ -114,7 +114,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): books.append(book) except: pass - + # Save the book list and it's create time. if books: self.config[self.name + '_last_download'] = time.time() @@ -126,21 +126,21 @@ class MobileReadStore(BasicStoreConfig, StorePlugin): class BookRef(SearchResult): - + def __init__(self): SearchResult.__init__(self) - + self.format = '' class MobeReadStoreDialog(QDialog, Ui_Dialog): - + def __init__(self, plugin, *args): QDialog.__init__(self, *args) self.setupUi(self) self.plugin = plugin - + self.model = BooksModel() self.results_view.setModel(self.model) self.results_view.model().set_books(self.plugin.get_book_list()) @@ -150,14 +150,14 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog): self.search_query.textChanged.connect(self.model.set_filter) self.results_view.model().total_changed.connect(self.total.setText) self.finished.connect(self.dialog_closed) - + self.restore_state() - + def open_store(self, index): result = self.results_view.model().get_book(index) if result: self.plugin.open(self, result.detail_item) - + def restore_state(self): geometry = self.plugin.config['store_mobileread_dialog_geometry'] if geometry: @@ -172,7 +172,7 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog): else: for i in xrange(self.results_view.model().columnCount()): self.results_view.resizeColumnToContents(i) - + self.results_view.model().sort_col = self.plugin.config.get('store_mobileread_dialog_sort_col', 0) self.results_view.model().sort_order = self.plugin.config.get('store_mobileread_dialog_sort_order', Qt.AscendingOrder) self.results_view.model().sort(self.results_view.model().sort_col, self.results_view.model().sort_order) @@ -189,7 +189,7 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog): class BooksModel(QAbstractItemModel): - + total_changed = pyqtSignal(unicode) HEADERS = [_('Title'), _('Author(s)'), _('Format')] @@ -205,7 +205,7 @@ class BooksModel(QAbstractItemModel): def set_books(self, books): self.books = books self.all_books = books - + self.sort(self.sort_col, self.sort_order) def get_book(self, index): @@ -214,11 +214,11 @@ class BooksModel(QAbstractItemModel): return self.books[row] else: return None - + def set_filter(self, filter): #self.layoutAboutToBeChanged.emit() self.beginResetModel() - + self.filter = unicode(filter) self.books = [] if self.filter: @@ -241,7 +241,7 @@ class BooksModel(QAbstractItemModel): self.endResetModel() #self.layoutChanged.emit() - + def index(self, row, column, parent=QModelIndex()): return self.createIndex(row, column) @@ -255,7 +255,7 @@ class BooksModel(QAbstractItemModel): def columnCount(self, *args): return len(self.HEADERS) - + def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return NONE @@ -295,7 +295,7 @@ class BooksModel(QAbstractItemModel): if not self.books: return - descending = order == Qt.DescendingOrder + descending = order == Qt.DescendingOrder self.books.sort(None, lambda x: sort_key(unicode(self.data_as_text(x, col))), descending) From 7a122f2b6e78ec7e149c9557f5bdb93aabf1e189 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 20 Apr 2011 09:45:20 -0600 Subject: [PATCH 17/32] The Journal.ie by Phil Burns --- recipes/the_journal.recipe | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 recipes/the_journal.recipe diff --git a/recipes/the_journal.recipe b/recipes/the_journal.recipe new file mode 100644 index 0000000000..e65d7e272e --- /dev/null +++ b/recipes/the_journal.recipe @@ -0,0 +1,26 @@ +__license__ = 'GPL v3' +__copyright__ = '2011 Phil Burns' +''' +TheJournal.ie +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class TheJournal(BasicNewsRecipe): + + __author_ = 'Phil Burns' + title = u'TheJournal.ie' + oldest_article = 1 + max_articles_per_feed = 100 + encoding = 'utf8' + language = 'en_IE' + timefmt = ' (%A, %B %d, %Y)' + + no_stylesheets = True + remove_tags = [dict(name='div', attrs={'class':'footer'}), + dict(name=['script', 'noscript'])] + + extra_css = 'p, div { margin: 0pt; border: 0pt; text-indent: 0.5em }' + + feeds = [ + (u'Latest News', u'http://www.thejournal.ie/feed/')] From 1e246c89d31ecc41f073ee225c90c53b61630a81 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 20 Apr 2011 09:51:10 -0600 Subject: [PATCH 18/32] ... --- src/calibre/ebooks/metadata/sources/overdrive.py | 2 +- src/calibre/gui2/metadata/bulk_download2.py | 3 ++- src/calibre/gui2/metadata/single.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py index 5f70802314..26c25b627f 100755 --- a/src/calibre/ebooks/metadata/sources/overdrive.py +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -43,7 +43,7 @@ class OverDrive(Source): def __init__(self, *args, **kwargs): Source.__init__(self, *args, **kwargs) - self.prefs.defaults['ignore_fields'] =['tags', 'pubdate', 'comments', 'identifier:isbn'] + self.prefs.defaults['ignore_fields'] =['tags', 'pubdate', 'comments'] def identify(self, log, result_queue, abort, title=None, authors=None, # {{{ identifiers={}, timeout=30): diff --git a/src/calibre/gui2/metadata/bulk_download2.py b/src/calibre/gui2/metadata/bulk_download2.py index 4aa4561078..2bbb177e14 100644 --- a/src/calibre/gui2/metadata/bulk_download2.py +++ b/src/calibre/gui2/metadata/bulk_download2.py @@ -310,7 +310,8 @@ def proceed(gui, job): def merge_result(oldmi, newmi): dummy = Metadata(_('Unknown')) for f in msprefs['ignore_fields']: - setattr(newmi, f, getattr(dummy, f)) + if ':' not in f: + setattr(newmi, f, getattr(dummy, f)) fields = set() for plugin in metadata_plugins(['identify']): fields |= plugin.touched_fields diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index d527dda022..63d4499966 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -326,7 +326,8 @@ class MetadataSingleDialogBase(ResizableDialog): mi = d.book dummy = Metadata(_('Unknown')) for f in msprefs['ignore_fields']: - setattr(mi, f, getattr(dummy, f)) + if ':' not in f: + setattr(mi, f, getattr(dummy, f)) if mi is not None: self.update_from_mi(mi) if d.cover_pixmap is not None: From dae6dcaa38a9fedcfd578068af89644af849f0a2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 20 Apr 2011 10:01:11 -0600 Subject: [PATCH 19/32] Implement ignore_fields for individual plugins (I'd forgotten to do this earlier). --- src/calibre/ebooks/metadata/sources/identify.py | 4 ++++ src/calibre/ebooks/metadata/sources/overdrive.py | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 6295efa0c0..4d21a0c210 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -382,7 +382,11 @@ def identify(log, abort, # {{{ log(plog) log('\n'+'*'*80) + dummy = Metadata(_('Unknown')) for i, result in enumerate(presults): + for f in plugin.prefs['ignore_fields']: + if ':' not in f: + setattr(result, f, getattr(dummy, f)) result.relevance_in_source = i result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable and plugin.get_cached_cover_url(result.identifiers) is not diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py index 26c25b627f..0affbdd805 100755 --- a/src/calibre/ebooks/metadata/sources/overdrive.py +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -41,10 +41,6 @@ class OverDrive(Source): supports_gzip_transfer_encoding = False cached_cover_url_is_reliable = True - def __init__(self, *args, **kwargs): - Source.__init__(self, *args, **kwargs) - self.prefs.defaults['ignore_fields'] =['tags', 'pubdate', 'comments'] - def identify(self, log, result_queue, abort, title=None, authors=None, # {{{ identifiers={}, timeout=30): ovrdrv_id = identifiers.get('overdrive', None) @@ -390,7 +386,10 @@ class OverDrive(Source): if pub_date: from calibre.utils.date import parse_date - mi.pubdate = parse_date(pub_date[0].strip()) + try: + mi.pubdate = parse_date(pub_date[0].strip()) + except: + pass if lang: lang = lang[0].strip().lower() mi.language = {'english':'en', 'french':'fr', 'german':'de', From b09ae3027915e27e0c935030f87e1d2da96744d7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 20 Apr 2011 10:43:08 -0600 Subject: [PATCH 20/32] Fix #764129 (Fetch Annotations (Toolbar) button not working.) --- src/calibre/gui2/actions/annotate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py index 48397936fb..f934a4a53c 100644 --- a/src/calibre/gui2/actions/annotate.py +++ b/src/calibre/gui2/actions/annotate.py @@ -22,7 +22,7 @@ class FetchAnnotationsAction(InterfaceAction): action_type = 'current' def genesis(self): - pass + self.qaction.triggered.connect(self.fetch_annotations) def fetch_annotations(self, *args): # Generate a path_map from selected ids @@ -52,6 +52,10 @@ class FetchAnnotationsAction(InterfaceAction): return path_map device = self.gui.device_manager.device + if not getattr(device, 'SUPPORTS_ANNOTATIONS', False): + return error_dialog(self.gui, _('Not supported'), + _('Fetching annotations is not supported for this device'), + show=True) if self.gui.current_view() is not self.gui.library_view: return error_dialog(self.gui, _('Use library only'), From f9ed8adb44e32991b885c9707de3c2bc07acc0b1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 20 Apr 2011 11:20:08 -0600 Subject: [PATCH 21/32] Shave another 0.1 secs of worker launch time on non linux platforms --- src/calibre/__init__.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 0bf48a8a97..91e1b57b38 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -3,8 +3,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import uuid, sys, os, re, logging, time, random, \ - __builtin__, warnings +import sys, os, re, logging, time, random, __builtin__, warnings from urllib import getproxies from urllib2 import unquote as urllib2_unquote __builtin__.__dict__['dynamic_property'] = lambda(func): func(None) @@ -15,15 +14,20 @@ from functools import partial warnings.simplefilter('ignore', DeprecationWarning) -from calibre.constants import iswindows, isosx, islinux, isfreebsd, isfrozen, \ - terminal_controller, preferred_encoding, \ - __appname__, __version__, __author__, \ - win32event, win32api, winerror, fcntl, \ - filesystem_encoding, plugins, config_dir +from calibre.constants import (iswindows, isosx, islinux, isfreebsd, isfrozen, + terminal_controller, preferred_encoding, + __appname__, __version__, __author__, + win32event, win32api, winerror, fcntl, + filesystem_encoding, plugins, config_dir) from calibre.startup import winutil, winutilerror, guess_type if islinux and not getattr(sys, 'frozen', False): - # Imported before PyQt4 to workaround PyQt4 util-linux conflict on gentoo + # Imported before PyQt4 to workaround PyQt4 util-linux conflict discovered on gentoo + # See http://bugs.gentoo.org/show_bug.cgi?id=317557 + # Importing uuid is slow so get rid of this at some point, maybe in a few + # years when even Debian has caught up + # Also remember to remove it from site.py in the binary builds + import uuid uuid.uuid4() if False: From 2897f63a0f311b05815e4e2be82ca86506a98c8b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 20 Apr 2011 12:48:22 -0600 Subject: [PATCH 22/32] More delay load optimizations to reduce worker startup time --- src/calibre/__init__.py | 44 +++++++++++++++++++----- src/calibre/constants.py | 26 ++++++++------ src/calibre/debug.py | 4 +-- src/calibre/ebooks/chm/reader.py | 2 +- src/calibre/ebooks/conversion/plumber.py | 4 ++- src/calibre/ebooks/fb2/fb2ml.py | 3 +- src/calibre/ebooks/metadata/__init__.py | 6 ++-- src/calibre/ebooks/metadata/fb2.py | 5 +-- src/calibre/ebooks/metadata/opf2.py | 8 ++--- src/calibre/ebooks/oeb/base.py | 5 ++- src/calibre/ebooks/oeb/reader.py | 2 +- src/calibre/ebooks/snb/snbfile.py | 5 +-- src/calibre/library/cli.py | 8 +++-- src/calibre/startup.py | 4 --- src/calibre/utils/config.py | 35 +++++++++++++------ 15 files changed, 104 insertions(+), 57 deletions(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 91e1b57b38..29c69a6799 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -3,9 +3,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import sys, os, re, logging, time, random, __builtin__, warnings -from urllib import getproxies -from urllib2 import unquote as urllib2_unquote +import sys, os, re, time, random, __builtin__, warnings __builtin__.__dict__['dynamic_property'] = lambda(func): func(None) from htmlentitydefs import name2codepoint from math import floor @@ -15,13 +13,12 @@ warnings.simplefilter('ignore', DeprecationWarning) from calibre.constants import (iswindows, isosx, islinux, isfreebsd, isfrozen, - terminal_controller, preferred_encoding, - __appname__, __version__, __author__, + preferred_encoding, __appname__, __version__, __author__, win32event, win32api, winerror, fcntl, filesystem_encoding, plugins, config_dir) -from calibre.startup import winutil, winutilerror, guess_type +from calibre.startup import winutil, winutilerror -if islinux and not getattr(sys, 'frozen', False): +if False and islinux and not getattr(sys, 'frozen', False): # Imported before PyQt4 to workaround PyQt4 util-linux conflict discovered on gentoo # See http://bugs.gentoo.org/show_bug.cgi?id=317557 # Importing uuid is slow so get rid of this at some point, maybe in a few @@ -33,8 +30,33 @@ if islinux and not getattr(sys, 'frozen', False): if False: # Prevent pyflakes from complaining winutil, winutilerror, __appname__, islinux, __version__ - fcntl, win32event, isfrozen, __author__, terminal_controller - winerror, win32api, isfreebsd, guess_type + fcntl, win32event, isfrozen, __author__ + winerror, win32api, isfreebsd + +_mt_inited = False +def _init_mimetypes(): + global _mt_inited + import mimetypes + mimetypes.init([P('mime.types')]) + _mt_inited = True + +def guess_type(*args, **kwargs): + import mimetypes + if not _mt_inited: + _init_mimetypes() + return mimetypes.guess_type(*args, **kwargs) + +def guess_all_extensions(*args, **kwargs): + import mimetypes + if not _mt_inited: + _init_mimetypes() + return mimetypes.guess_all_extensions(*args, **kwargs) + +def get_types_map(): + import mimetypes + if not _mt_inited: + _init_mimetypes() + return mimetypes.types_map def to_unicode(raw, encoding='utf-8', errors='strict'): if isinstance(raw, unicode): @@ -182,6 +204,7 @@ class CommandLineError(Exception): pass def setup_cli_handlers(logger, level): + import logging if os.environ.get('CALIBRE_WORKER', None) is not None and logger.handlers: return logger.setLevel(level) @@ -243,6 +266,7 @@ def extract(path, dir): extractor(path, dir) def get_proxies(debug=True): + from urllib import getproxies proxies = getproxies() for key, proxy in list(proxies.items()): if not proxy or '..' in proxy: @@ -552,6 +576,8 @@ def get_download_filename(url, cookie_file=None): Get a local filename for a URL using the content disposition header ''' from contextlib import closing + from urllib2 import unquote as urllib2_unquote + filename = '' br = browser() diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 6f26a63940..62612d9c66 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -1,23 +1,27 @@ +from future_builtins import map + __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' -__appname__ = 'calibre' -__version__ = '0.7.56' -__author__ = "Kovid Goyal " - -import re, importlib -_ver = __version__.split('.') -_ver = [int(re.search(r'(\d+)', x).group(1)) for x in _ver] -numeric_version = tuple(_ver) +__appname__ = u'calibre' +numeric_version = (0, 7, 56) +__version__ = u'.'.join(map(unicode, numeric_version)) +__author__ = u"Kovid Goyal " ''' Various run time constants. ''' -import sys, locale, codecs, os -from calibre.utils.terminfo import TerminalController +import sys, locale, codecs, os, importlib + +_tc = None +def terminal_controller(): + global _tc + if _tc is None: + from calibre.utils.terminfo import TerminalController + _tc = TerminalController(sys.stdout) + return _tc -terminal_controller = TerminalController(sys.stdout) iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower() isosx = 'darwin' in sys.platform.lower() diff --git a/src/calibre/debug.py b/src/calibre/debug.py index 86a0477811..8d65c37bbf 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -106,7 +106,7 @@ def migrate(old, new): from calibre.library.database import LibraryDatabase from calibre.library.database2 import LibraryDatabase2 from calibre.utils.terminfo import ProgressBar - from calibre import terminal_controller + from calibre.constants import terminal_controller class Dummy(ProgressBar): def setLabelText(self, x): pass def setAutoReset(self, y): pass @@ -119,7 +119,7 @@ def migrate(old, new): db = LibraryDatabase(old) db2 = LibraryDatabase2(new) - db2.migrate_old(db, Dummy(terminal_controller, 'Migrating database...')) + db2.migrate_old(db, Dummy(terminal_controller(), 'Migrating database...')) prefs['library_path'] = os.path.abspath(new) print 'Database migrated to', os.path.abspath(new) diff --git a/src/calibre/ebooks/chm/reader.py b/src/calibre/ebooks/chm/reader.py index 7c9a6bf48a..24814a34f9 100644 --- a/src/calibre/ebooks/chm/reader.py +++ b/src/calibre/ebooks/chm/reader.py @@ -5,8 +5,8 @@ __copyright__ = '2008, Kovid Goyal ,' \ ' and Alex Bramley .' import os, re -from mimetypes import guess_type as guess_mimetype +from calibre import guess_type as guess_mimetype from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString from calibre.constants import iswindows, filesystem_encoding from calibre.utils.chm.chm import CHMFile diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 8706d6eb2b..96ea3e5884 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -14,7 +14,8 @@ from calibre.ebooks.conversion.preprocess import HTMLPreProcessor from calibre.ptempfile import PersistentTemporaryDirectory from calibre.utils.date import parse_date from calibre.utils.zipfile import ZipFile -from calibre import extract, walk, isbytestring, filesystem_encoding +from calibre import (extract, walk, isbytestring, filesystem_encoding, + get_types_map) from calibre.constants import __version__ DEBUG_README=u''' @@ -877,6 +878,7 @@ OptionRecommendation(name='sr3_replace', self.flush() import cssutils, logging cssutils.log.setLevel(logging.WARN) + get_types_map() # Ensure the mimetypes module is intialized if self.opts.debug_pipeline is not None: self.opts.verbose = max(self.opts.verbose, 4) diff --git a/src/calibre/ebooks/fb2/fb2ml.py b/src/calibre/ebooks/fb2/fb2ml.py index 385c4a5310..b45f8f9f9e 100644 --- a/src/calibre/ebooks/fb2/fb2ml.py +++ b/src/calibre/ebooks/fb2/fb2ml.py @@ -10,7 +10,6 @@ Transform OEB content into FB2 markup from base64 import b64encode from datetime import datetime -from mimetypes import types_map import re import uuid @@ -259,7 +258,7 @@ class FB2MLizer(object): continue if item.media_type in OEB_RASTER_IMAGES: try: - if not item.media_type == types_map['.jpeg'] or not item.media_type == types_map['.jpg']: + if item.media_type != 'image/jpeg': im = Image() im.load(item.data) im.set_compression_quality(70) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 6078a0aa94..2ae5f3ade5 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -6,11 +6,11 @@ __docformat__ = 'restructuredtext en' """ Provides abstraction for metadata reading.writing from a variety of ebook formats. """ -import os, mimetypes, sys, re +import os, sys, re from urllib import unquote, quote from urlparse import urlparse -from calibre import relpath +from calibre import relpath, guess_type from calibre.utils.config import tweaks @@ -118,7 +118,7 @@ class Resource(object): self.path = None self.fragment = '' try: - self.mime_type = mimetypes.guess_type(href_or_path)[0] + self.mime_type = guess_type(href_or_path)[0] except: self.mime_type = None if self.mime_type is None: diff --git a/src/calibre/ebooks/metadata/fb2.py b/src/calibre/ebooks/metadata/fb2.py index 2d6192f949..21f15b05ae 100644 --- a/src/calibre/ebooks/metadata/fb2.py +++ b/src/calibre/ebooks/metadata/fb2.py @@ -5,11 +5,12 @@ __copyright__ = '2008, Anatoly Shipitsin ' '''Read meta information from fb2 files''' -import mimetypes, os +import os from base64 import b64decode from lxml import etree from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.chardet import xml_to_unicode +from calibre import guess_all_extensions XLINK_NS = 'http://www.w3.org/1999/xlink' def XLINK(name): @@ -71,7 +72,7 @@ def get_metadata(stream): binary = XPath('//fb2:binary[@id="%s"]'%id)(root) if binary: mt = binary[0].get('content-type', 'image/jpeg') - exts = mimetypes.guess_all_extensions(mt) + exts = guess_all_extensions(mt) if not exts: exts = ['.jpg'] cdata = (exts[0][1:], b64decode(tostring(binary[0]))) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index d360451e2e..58c887bfdb 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' lxml based OPF parser. ''' -import re, sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO, json +import re, sys, unittest, functools, os, uuid, glob, cStringIO, json from urllib import unquote from urlparse import urlparse @@ -20,7 +20,7 @@ from calibre.ebooks.metadata import string_to_authors, MetaInformation, check_is from calibre.ebooks.metadata.book.base import Metadata from calibre.utils.date import parse_date, isoformat from calibre.utils.localization import get_lang -from calibre import prints +from calibre import prints, guess_type from calibre.utils.cleantext import clean_ascii_chars class Resource(object): # {{{ @@ -42,7 +42,7 @@ class Resource(object): # {{{ self.path = None self.fragment = '' try: - self.mime_type = mimetypes.guess_type(href_or_path)[0] + self.mime_type = guess_type(href_or_path)[0] except: self.mime_type = None if self.mime_type is None: @@ -1000,7 +1000,7 @@ class OPF(object): # {{{ for t in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'): for item in self.guide: if item.type.lower() == t: - self.create_manifest_item(item.href(), mimetypes.guess_type(path)[0]) + self.create_manifest_item(item.href(), guess_type(path)[0]) return property(fget=fget, fset=fset) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index ce75c97d78..f2c9696976 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -8,7 +8,6 @@ __copyright__ = '2008, Marshall T. Vandegrift ' __docformat__ = 'restructuredtext en' import os, re, uuid, logging -from mimetypes import types_map from collections import defaultdict from itertools import count from urlparse import urldefrag, urlparse, urlunparse, urljoin @@ -20,7 +19,7 @@ from calibre.translations.dynamic import translate from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.oeb.entitydefs import ENTITYDEFS from calibre.ebooks.conversion.preprocess import CSSPreProcessor -from calibre import isbytestring, as_unicode +from calibre import isbytestring, as_unicode, get_types_map RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True) @@ -247,7 +246,7 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False): el.attrib['style'] = repl - +types_map = get_types_map() EPUB_MIME = types_map['.epub'] XHTML_MIME = types_map['.xhtml'] CSS_MIME = types_map['.css'] diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 1c42a5a242..6c10436038 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -10,7 +10,6 @@ import sys, os, uuid, copy, re, cStringIO from itertools import izip from urlparse import urldefrag, urlparse from urllib import unquote as urlunquote -from mimetypes import guess_type from collections import defaultdict from lxml import etree @@ -29,6 +28,7 @@ from calibre.ebooks.oeb.entitydefs import ENTITYDEFS from calibre.utils.localization import get_lang from calibre.ptempfile import TemporaryDirectory from calibre.constants import __appname__, __version__ +from calibre import guess_type __all__ = ['OEBReader'] diff --git a/src/calibre/ebooks/snb/snbfile.py b/src/calibre/ebooks/snb/snbfile.py index 1a0986baf4..be4e537825 100644 --- a/src/calibre/ebooks/snb/snbfile.py +++ b/src/calibre/ebooks/snb/snbfile.py @@ -5,7 +5,8 @@ __copyright__ = '2010, Li Fanxi ' __docformat__ = 'restructuredtext en' import sys, struct, zlib, bz2, os -from mimetypes import types_map + +from calibre import guess_type class FileStream: def IsBinary(self): @@ -180,7 +181,7 @@ class SNBFile: file = open(os.path.join(path, fname), 'wb') file.write(f.fileBody) file.close() - fileNames.append((fname, types_map[ext])) + fileNames.append((fname, guess_type('a'+ext)[0])) return fileNames def Output(self, outputFile): diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index b1a8236151..61e7ec334d 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -10,8 +10,7 @@ Command line interface to the calibre database. import sys, os, cStringIO, re from textwrap import TextWrapper -from calibre import terminal_controller, preferred_encoding, prints, \ - isbytestring +from calibre import preferred_encoding, prints, isbytestring from calibre.utils.config import OptionParser, prefs, tweaks from calibre.ebooks.metadata.meta import get_metadata from calibre.library.database2 import LibraryDatabase2 @@ -53,6 +52,8 @@ def get_db(dbpath, options): def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, separator, prefix, subtitle='Books in the calibre database'): + from calibre.constants import terminal_controller as tc + terminal_controller = tc() if sort_by: db.sort(sort_by, ascending) if search_text: @@ -1087,6 +1088,9 @@ def command_list_categories(args, dbpath): fields = ['category', 'tag_name', 'count', 'rating'] def do_list(): + from calibre.constants import terminal_controller as tc + terminal_controller = tc() + separator = ' ' widths = list(map(lambda x : 0, fields)) for i in data: diff --git a/src/calibre/startup.py b/src/calibre/startup.py index c883c43e8a..fd9ef01141 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -163,10 +163,6 @@ if not _run_once: __builtin__.__dict__['icu_upper'] = icu_upper __builtin__.__dict__['icu_title'] = title_case - import mimetypes - mimetypes.init([P('mime.types')]) - guess_type = mimetypes.guess_type - def test_lopen(): from calibre.ptempfile import TemporaryDirectory from calibre import CurrentDir diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 66316d051b..6f2840e95e 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -6,15 +6,15 @@ __docformat__ = 'restructuredtext en' ''' Manage application-wide preferences. ''' -import os, re, cPickle, textwrap, traceback, plistlib, json, base64, datetime +import os, re, cPickle, traceback, base64, datetime from copy import deepcopy from functools import partial from optparse import OptionParser as _OptionParser from optparse import IndentedHelpFormatter from collections import defaultdict -from calibre.constants import terminal_controller, config_dir, CONFIG_DIR_MODE, \ - __appname__, __version__, __author__ +from calibre.constants import (config_dir, CONFIG_DIR_MODE, __appname__, + __version__, __author__, terminal_controller) from calibre.utils.lock import LockError, ExclusiveFile plugin_dir = os.path.join(config_dir, 'plugins') @@ -29,23 +29,28 @@ def check_config_write_access(): class CustomHelpFormatter(IndentedHelpFormatter): def format_usage(self, usage): - return _("%sUsage%s: %s\n") % (terminal_controller.BLUE, terminal_controller.NORMAL, usage) + tc = terminal_controller() + return _("%sUsage%s: %s\n") % (tc.BLUE, tc.NORMAL, usage) def format_heading(self, heading): - return "%*s%s%s%s:\n" % (self.current_indent, terminal_controller.BLUE, - "", heading, terminal_controller.NORMAL) + tc = terminal_controller() + return "%*s%s%s%s:\n" % (self.current_indent, tc.BLUE, + "", heading, tc.NORMAL) def format_option(self, option): + import textwrap + tc = terminal_controller() + result = [] opts = self.option_strings[option] opt_width = self.help_position - self.current_indent - 2 if len(opts) > opt_width: opts = "%*s%s\n" % (self.current_indent, "", - terminal_controller.GREEN+opts+terminal_controller.NORMAL) + tc.GREEN+opts+tc.NORMAL) indent_first = self.help_position else: # start help on same line as opts - opts = "%*s%-*s " % (self.current_indent, "", opt_width + len(terminal_controller.GREEN + terminal_controller.NORMAL), - terminal_controller.GREEN + opts + terminal_controller.NORMAL) + opts = "%*s%-*s " % (self.current_indent, "", opt_width + + len(tc.GREEN + tc.NORMAL), tc.GREEN + opts + tc.NORMAL) indent_first = 0 result.append(opts) if option.help: @@ -71,9 +76,12 @@ class OptionParser(_OptionParser): gui_mode=False, conflict_handler='resolve', **kwds): + import textwrap + tc = terminal_controller() + usage = textwrap.dedent(usage) if epilog is None: - epilog = _('Created by ')+terminal_controller.RED+__author__+terminal_controller.NORMAL + epilog = _('Created by ')+tc.RED+__author__+tc.NORMAL usage += '\n\n'+_('''Whenever you pass arguments to %prog that have spaces in them, ''' '''enclose the arguments in quotation marks.''') _OptionParser.__init__(self, usage=usage, version=version, epilog=epilog, @@ -579,9 +587,11 @@ class XMLConfig(dict): self.refresh() def raw_to_object(self, raw): + import plistlib return plistlib.readPlistFromString(raw) def to_raw(self): + import plistlib return plistlib.writePlistToString(self) def refresh(self): @@ -601,6 +611,7 @@ class XMLConfig(dict): self.update(d) def __getitem__(self, key): + import plistlib try: ans = dict.__getitem__(self, key) if isinstance(ans, plistlib.Data): @@ -610,6 +621,7 @@ class XMLConfig(dict): return self.defaults.get(key, None) def get(self, key, default=None): + import plistlib try: ans = dict.__getitem__(self, key) if isinstance(ans, plistlib.Data): @@ -619,6 +631,7 @@ class XMLConfig(dict): return self.defaults.get(key, default) def __setitem__(self, key, val): + import plistlib if isinstance(val, (bytes, str)): val = plistlib.Data(val) dict.__setitem__(self, key, val) @@ -667,9 +680,11 @@ class JSONConfig(XMLConfig): EXTENSION = '.json' def raw_to_object(self, raw): + import json return json.loads(raw.decode('utf-8'), object_hook=from_json) def to_raw(self): + import json return json.dumps(self, indent=2, default=to_json) def __getitem__(self, key): From cdd47f4b10aecd9a55b13fc92e46e60e8dca6650 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 20 Apr 2011 13:14:19 -0600 Subject: [PATCH 23/32] And some more delay load optimizations --- src/calibre/utils/config.py | 468 +----------------------------- src/calibre/utils/config_base.py | 467 +++++++++++++++++++++++++++++ src/calibre/utils/icu.py | 2 +- src/calibre/utils/localization.py | 2 +- 4 files changed, 480 insertions(+), 459 deletions(-) create mode 100644 src/calibre/utils/config_base.py diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 6f2840e95e..87682c9792 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -6,22 +6,25 @@ __docformat__ = 'restructuredtext en' ''' Manage application-wide preferences. ''' -import os, re, cPickle, traceback, base64, datetime +import os, re, cPickle, base64, datetime, json, plistlib from copy import deepcopy -from functools import partial from optparse import OptionParser as _OptionParser from optparse import IndentedHelpFormatter -from collections import defaultdict from calibre.constants import (config_dir, CONFIG_DIR_MODE, __appname__, __version__, __author__, terminal_controller) -from calibre.utils.lock import LockError, ExclusiveFile +from calibre.utils.lock import ExclusiveFile +from calibre.utils.config_base import (make_config_dir, Option, OptionValues, + OptionSet, ConfigInterface, Config, prefs, StringConfig, ConfigProxy, + read_raw_tweaks, read_tweaks, write_tweaks, tweaks) -plugin_dir = os.path.join(config_dir, 'plugins') +if False: + # Make pyflakes happy + Config, ConfigProxy, Option, OptionValues, StringConfig + OptionSet, ConfigInterface, read_tweaks, write_tweaks + read_raw_tweaks, tweaks -def make_config_dir(): - if not os.path.exists(plugin_dir): - os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE) +test_eight_code = tweaks.get('test_eight_code', False) def check_config_write_access(): return os.access(config_dir, os.W_OK) and os.access(config_dir, os.X_OK) @@ -154,353 +157,6 @@ class OptionParser(_OptionParser): upper.__dict__[dest] = lower.__dict__[dest] - -class Option(object): - - def __init__(self, name, switches=[], help='', type=None, choices=None, - check=None, group=None, default=None, action=None, metavar=None): - if choices: - type = 'choice' - - self.name = name - self.switches = switches - self.help = help.replace('%default', repr(default)) if help else None - self.type = type - if self.type is None and action is None and choices is None: - if isinstance(default, float): - self.type = 'float' - elif isinstance(default, int) and not isinstance(default, bool): - self.type = 'int' - - self.choices = choices - self.check = check - self.group = group - self.default = default - self.action = action - self.metavar = metavar - - def __eq__(self, other): - return self.name == getattr(other, 'name', other) - - def __repr__(self): - return 'Option: '+self.name - - def __str__(self): - return repr(self) - -class OptionValues(object): - - def copy(self): - return deepcopy(self) - -class OptionSet(object): - - OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}', - re.DOTALL|re.IGNORECASE) - - def __init__(self, description=''): - self.description = description - self.defaults = {} - self.preferences = [] - self.group_list = [] - self.groups = {} - self.set_buffer = {} - - def has_option(self, name_or_option_object): - if name_or_option_object in self.preferences: - return True - for p in self.preferences: - if p.name == name_or_option_object: - return True - return False - - def get_option(self, name_or_option_object): - idx = self.preferences.index(name_or_option_object) - if idx > -1: - return self.preferences[idx] - for p in self.preferences: - if p.name == name_or_option_object: - return p - - def add_group(self, name, description=''): - if name in self.group_list: - raise ValueError('A group by the name %s already exists in this set'%name) - self.groups[name] = description - self.group_list.append(name) - return partial(self.add_opt, group=name) - - def update(self, other): - for name in other.groups.keys(): - self.groups[name] = other.groups[name] - if name not in self.group_list: - self.group_list.append(name) - for pref in other.preferences: - if pref in self.preferences: - self.preferences.remove(pref) - self.preferences.append(pref) - - def smart_update(self, opts1, opts2): - ''' - Updates the preference values in opts1 using only the non-default preference values in opts2. - ''' - for pref in self.preferences: - new = getattr(opts2, pref.name, pref.default) - if new != pref.default: - setattr(opts1, pref.name, new) - - def remove_opt(self, name): - if name in self.preferences: - self.preferences.remove(name) - - - def add_opt(self, name, switches=[], help=None, type=None, choices=None, - group=None, default=None, action=None, metavar=None): - ''' - Add an option to this section. - - :param name: The name of this option. Must be a valid Python identifier. - Must also be unique in this OptionSet and all its subsets. - :param switches: List of command line switches for this option - (as supplied to :module:`optparse`). If empty, this - option will not be added to the command line parser. - :param help: Help text. - :param type: Type checking of option values. Supported types are: - `None, 'choice', 'complex', 'float', 'int', 'string'`. - :param choices: List of strings or `None`. - :param group: Group this option belongs to. You must previously - have created this group with a call to :method:`add_group`. - :param default: The default value for this option. - :param action: The action to pass to optparse. Supported values are: - `None, 'count'`. For choices and boolean options, - action is automatically set correctly. - ''' - pref = Option(name, switches=switches, help=help, type=type, choices=choices, - group=group, default=default, action=action, metavar=None) - if group is not None and group not in self.groups.keys(): - raise ValueError('Group %s has not been added to this section'%group) - if pref in self.preferences: - raise ValueError('An option with the name %s already exists in this set.'%name) - self.preferences.append(pref) - self.defaults[name] = default - - def option_parser(self, user_defaults=None, usage='', gui_mode=False): - parser = OptionParser(usage, gui_mode=gui_mode) - groups = defaultdict(lambda : parser) - for group, desc in self.groups.items(): - groups[group] = parser.add_option_group(group.upper(), desc) - - for pref in self.preferences: - if not pref.switches: - continue - g = groups[pref.group] - action = pref.action - if action is None: - action = 'store' - if pref.default is True or pref.default is False: - action = 'store_' + ('false' if pref.default else 'true') - args = dict( - dest=pref.name, - help=pref.help, - metavar=pref.metavar, - type=pref.type, - choices=pref.choices, - default=getattr(user_defaults, pref.name, pref.default), - action=action, - ) - g.add_option(*pref.switches, **args) - - - return parser - - def get_override_section(self, src): - match = self.OVERRIDE_PAT.search(src) - if match: - return match.group() - return '' - - def parse_string(self, src): - options = {'cPickle':cPickle} - if src is not None: - try: - if not isinstance(src, unicode): - src = src.decode('utf-8') - exec src in options - except: - print 'Failed to parse options string:' - print repr(src) - traceback.print_exc() - opts = OptionValues() - for pref in self.preferences: - val = options.get(pref.name, pref.default) - formatter = __builtins__.get(pref.type, None) - if callable(formatter): - val = formatter(val) - setattr(opts, pref.name, val) - - return opts - - def render_group(self, name, desc, opts): - prefs = [pref for pref in self.preferences if pref.group == name] - lines = ['### Begin group: %s'%(name if name else 'DEFAULT')] - if desc: - lines += map(lambda x: '# '+x, desc.split('\n')) - lines.append(' ') - for pref in prefs: - lines.append('# '+pref.name.replace('_', ' ')) - if pref.help: - lines += map(lambda x: '# ' + x, pref.help.split('\n')) - lines.append('%s = %s'%(pref.name, - self.serialize_opt(getattr(opts, pref.name, pref.default)))) - lines.append(' ') - return '\n'.join(lines) - - def serialize_opt(self, val): - if val is val is True or val is False or val is None or \ - isinstance(val, (int, float, long, basestring)): - return repr(val) - from PyQt4.QtCore import QString - if isinstance(val, QString): - return repr(unicode(val)) - pickle = cPickle.dumps(val, -1) - return 'cPickle.loads(%s)'%repr(pickle) - - def serialize(self, opts): - src = '# %s\n\n'%(self.description.replace('\n', '\n# ')) - groups = [self.render_group(name, self.groups.get(name, ''), opts) \ - for name in [None] + self.group_list] - return src + '\n\n'.join(groups) - -class ConfigInterface(object): - - def __init__(self, description): - self.option_set = OptionSet(description=description) - self.add_opt = self.option_set.add_opt - self.add_group = self.option_set.add_group - self.remove_opt = self.remove = self.option_set.remove_opt - self.parse_string = self.option_set.parse_string - self.get_option = self.option_set.get_option - self.preferences = self.option_set.preferences - - def update(self, other): - self.option_set.update(other.option_set) - - def option_parser(self, usage='', gui_mode=False): - return self.option_set.option_parser(user_defaults=self.parse(), - usage=usage, gui_mode=gui_mode) - - def smart_update(self, opts1, opts2): - self.option_set.smart_update(opts1, opts2) - - -class Config(ConfigInterface): - ''' - A file based configuration. - ''' - - def __init__(self, basename, description=''): - ConfigInterface.__init__(self, description) - self.config_file_path = os.path.join(config_dir, basename+'.py') - - - def parse(self): - src = '' - if os.path.exists(self.config_file_path): - try: - with ExclusiveFile(self.config_file_path) as f: - try: - src = f.read().decode('utf-8') - except ValueError: - print "Failed to parse", self.config_file_path - traceback.print_exc() - except LockError: - raise IOError('Could not lock config file: %s'%self.config_file_path) - return self.option_set.parse_string(src) - - def as_string(self): - if not os.path.exists(self.config_file_path): - return '' - try: - with ExclusiveFile(self.config_file_path) as f: - return f.read().decode('utf-8') - except LockError: - raise IOError('Could not lock config file: %s'%self.config_file_path) - - def set(self, name, val): - if not self.option_set.has_option(name): - raise ValueError('The option %s is not defined.'%name) - try: - if not os.path.exists(config_dir): - make_config_dir() - with ExclusiveFile(self.config_file_path) as f: - src = f.read() - opts = self.option_set.parse_string(src) - setattr(opts, name, val) - footer = self.option_set.get_override_section(src) - src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' - f.seek(0) - f.truncate() - if isinstance(src, unicode): - src = src.encode('utf-8') - f.write(src) - except LockError: - raise IOError('Could not lock config file: %s'%self.config_file_path) - -class StringConfig(ConfigInterface): - ''' - A string based configuration - ''' - - def __init__(self, src, description=''): - ConfigInterface.__init__(self, description) - self.src = src - - def parse(self): - return self.option_set.parse_string(self.src) - - def set(self, name, val): - if not self.option_set.has_option(name): - raise ValueError('The option %s is not defined.'%name) - opts = self.option_set.parse_string(self.src) - setattr(opts, name, val) - footer = self.option_set.get_override_section(self.src) - self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' - -class ConfigProxy(object): - ''' - A Proxy to minimize file reads for widely used config settings - ''' - - def __init__(self, config): - self.__config = config - self.__opts = None - - @property - def defaults(self): - return self.__config.option_set.defaults - - def refresh(self): - self.__opts = self.__config.parse() - - def __getitem__(self, key): - return self.get(key) - - def __setitem__(self, key, val): - return self.set(key, val) - - def get(self, key): - if self.__opts is None: - self.refresh() - return getattr(self.__opts, key) - - def set(self, key, val): - if self.__opts is None: - self.refresh() - setattr(self.__opts, key, val) - return self.__config.set(key, val) - - def help(self, key): - return self.__config.get_option(key).help - class DynamicConfig(dict): ''' A replacement for QSettings that supports dynamic config keys. @@ -587,11 +243,9 @@ class XMLConfig(dict): self.refresh() def raw_to_object(self, raw): - import plistlib return plistlib.readPlistFromString(raw) def to_raw(self): - import plistlib return plistlib.writePlistToString(self) def refresh(self): @@ -611,7 +265,6 @@ class XMLConfig(dict): self.update(d) def __getitem__(self, key): - import plistlib try: ans = dict.__getitem__(self, key) if isinstance(ans, plistlib.Data): @@ -621,7 +274,6 @@ class XMLConfig(dict): return self.defaults.get(key, None) def get(self, key, default=None): - import plistlib try: ans = dict.__getitem__(self, key) if isinstance(ans, plistlib.Data): @@ -631,7 +283,6 @@ class XMLConfig(dict): return self.defaults.get(key, default) def __setitem__(self, key, val): - import plistlib if isinstance(val, (bytes, str)): val = plistlib.Data(val) dict.__setitem__(self, key, val) @@ -680,11 +331,9 @@ class JSONConfig(XMLConfig): EXTENSION = '.json' def raw_to_object(self, raw): - import json return json.loads(raw.decode('utf-8'), object_hook=from_json) def to_raw(self): - import json return json.dumps(self, indent=2, default=to_json) def __getitem__(self, key): @@ -705,101 +354,6 @@ class JSONConfig(XMLConfig): -def _prefs(): - c = Config('global', 'calibre wide preferences') - c.add_opt('database_path', - default=os.path.expanduser('~/library1.db'), - help=_('Path to the database in which books are stored')) - c.add_opt('filename_pattern', default=ur'(?P.+) - (?P<author>[^_]+)', - help=_('Pattern to guess metadata from filenames')) - c.add_opt('isbndb_com_key', default='', - help=_('Access key for isbndb.com')) - c.add_opt('network_timeout', default=5, - help=_('Default timeout for network operations (seconds)')) - c.add_opt('library_path', default=None, - help=_('Path to directory in which your library of books is stored')) - c.add_opt('language', default=None, - help=_('The language in which to display the user interface')) - c.add_opt('output_format', default='EPUB', - help=_('The default output format for ebook conversions.')) - c.add_opt('input_format_order', default=['EPUB', 'MOBI', 'LIT', 'PRC', - 'FB2', 'HTML', 'HTM', 'XHTM', 'SHTML', 'XHTML', 'ZIP', 'ODT', 'RTF', 'PDF', - 'TXT'], - help=_('Ordered list of formats to prefer for input.')) - c.add_opt('read_file_metadata', default=True, - help=_('Read metadata from files')) - c.add_opt('worker_process_priority', default='normal', - help=_('The priority of worker processes. A higher priority ' - 'means they run faster and consume more resources. ' - 'Most tasks like conversion/news download/adding books/etc. ' - 'are affected by this setting.')) - c.add_opt('swap_author_names', default=False, - help=_('Swap author first and last names when reading metadata')) - c.add_opt('add_formats_to_existing', default=False, - help=_('Add new formats to existing book records')) - c.add_opt('installation_uuid', default=None, help='Installation UUID') - c.add_opt('new_book_tags', default=[], help=_('Tags to apply to books added to the library')) - - # these are here instead of the gui preferences because calibredb and - # calibre server can execute searches - c.add_opt('saved_searches', default={}, help=_('List of named saved searches')) - c.add_opt('user_categories', default={}, help=_('User-created tag browser categories')) - c.add_opt('manage_device_metadata', default='manual', - help=_('How and when calibre updates metadata on the device.')) - c.add_opt('limit_search_columns', default=False, - help=_('When searching for text without using lookup ' - 'prefixes, as for example, Red instead of title:Red, ' - 'limit the columns searched to those named below.')) - c.add_opt('limit_search_columns_to', - default=['title', 'authors', 'tags', 'series', 'publisher'], - help=_('Choose columns to be searched when not using prefixes, ' - 'as for example, when searching for Redd instead of ' - 'title:Red. Enter a list of search/lookup names ' - 'separated by commas. Only takes effect if you set the option ' - 'to limit search columns above.')) - - c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') - return c - -prefs = ConfigProxy(_prefs()) -if prefs['installation_uuid'] is None: - import uuid - prefs['installation_uuid'] = str(uuid.uuid4()) - -# Read tweaks -def read_raw_tweaks(): - make_config_dir() - default_tweaks = P('default_tweaks.py', data=True, - allow_user_override=False) - tweaks_file = os.path.join(config_dir, 'tweaks.py') - if not os.path.exists(tweaks_file): - with open(tweaks_file, 'wb') as f: - f.write(default_tweaks) - with open(tweaks_file, 'rb') as f: - return default_tweaks, f.read() - -def read_tweaks(): - default_tweaks, tweaks = read_raw_tweaks() - l, g = {}, {} - try: - exec tweaks in g, l - except: - print 'Failed to load custom tweaks file' - traceback.print_exc() - dl, dg = {}, {} - exec default_tweaks in dg, dl - dl.update(l) - return dl - -def write_tweaks(raw): - make_config_dir() - tweaks_file = os.path.join(config_dir, 'tweaks.py') - with open(tweaks_file, 'wb') as f: - f.write(raw) - - -tweaks = read_tweaks() -test_eight_code = tweaks.get('test_eight_code', False) def migrate(): if hasattr(os, 'geteuid') and os.geteuid() == 0: diff --git a/src/calibre/utils/config_base.py b/src/calibre/utils/config_base.py new file mode 100644 index 0000000000..7660370353 --- /dev/null +++ b/src/calibre/utils/config_base.py @@ -0,0 +1,467 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' +__docformat__ = 'restructuredtext en' + +import os, re, cPickle, traceback +from functools import partial +from collections import defaultdict +from copy import deepcopy + +from calibre.utils.lock import LockError, ExclusiveFile +from calibre.constants import config_dir, CONFIG_DIR_MODE + +plugin_dir = os.path.join(config_dir, 'plugins') + +def make_config_dir(): + if not os.path.exists(plugin_dir): + os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE) + +class Option(object): + + def __init__(self, name, switches=[], help='', type=None, choices=None, + check=None, group=None, default=None, action=None, metavar=None): + if choices: + type = 'choice' + + self.name = name + self.switches = switches + self.help = help.replace('%default', repr(default)) if help else None + self.type = type + if self.type is None and action is None and choices is None: + if isinstance(default, float): + self.type = 'float' + elif isinstance(default, int) and not isinstance(default, bool): + self.type = 'int' + + self.choices = choices + self.check = check + self.group = group + self.default = default + self.action = action + self.metavar = metavar + + def __eq__(self, other): + return self.name == getattr(other, 'name', other) + + def __repr__(self): + return 'Option: '+self.name + + def __str__(self): + return repr(self) + +class OptionValues(object): + + def copy(self): + return deepcopy(self) + +class OptionSet(object): + + OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}', + re.DOTALL|re.IGNORECASE) + + def __init__(self, description=''): + self.description = description + self.defaults = {} + self.preferences = [] + self.group_list = [] + self.groups = {} + self.set_buffer = {} + + def has_option(self, name_or_option_object): + if name_or_option_object in self.preferences: + return True + for p in self.preferences: + if p.name == name_or_option_object: + return True + return False + + def get_option(self, name_or_option_object): + idx = self.preferences.index(name_or_option_object) + if idx > -1: + return self.preferences[idx] + for p in self.preferences: + if p.name == name_or_option_object: + return p + + def add_group(self, name, description=''): + if name in self.group_list: + raise ValueError('A group by the name %s already exists in this set'%name) + self.groups[name] = description + self.group_list.append(name) + return partial(self.add_opt, group=name) + + def update(self, other): + for name in other.groups.keys(): + self.groups[name] = other.groups[name] + if name not in self.group_list: + self.group_list.append(name) + for pref in other.preferences: + if pref in self.preferences: + self.preferences.remove(pref) + self.preferences.append(pref) + + def smart_update(self, opts1, opts2): + ''' + Updates the preference values in opts1 using only the non-default preference values in opts2. + ''' + for pref in self.preferences: + new = getattr(opts2, pref.name, pref.default) + if new != pref.default: + setattr(opts1, pref.name, new) + + def remove_opt(self, name): + if name in self.preferences: + self.preferences.remove(name) + + + def add_opt(self, name, switches=[], help=None, type=None, choices=None, + group=None, default=None, action=None, metavar=None): + ''' + Add an option to this section. + + :param name: The name of this option. Must be a valid Python identifier. + Must also be unique in this OptionSet and all its subsets. + :param switches: List of command line switches for this option + (as supplied to :module:`optparse`). If empty, this + option will not be added to the command line parser. + :param help: Help text. + :param type: Type checking of option values. Supported types are: + `None, 'choice', 'complex', 'float', 'int', 'string'`. + :param choices: List of strings or `None`. + :param group: Group this option belongs to. You must previously + have created this group with a call to :method:`add_group`. + :param default: The default value for this option. + :param action: The action to pass to optparse. Supported values are: + `None, 'count'`. For choices and boolean options, + action is automatically set correctly. + ''' + pref = Option(name, switches=switches, help=help, type=type, choices=choices, + group=group, default=default, action=action, metavar=None) + if group is not None and group not in self.groups.keys(): + raise ValueError('Group %s has not been added to this section'%group) + if pref in self.preferences: + raise ValueError('An option with the name %s already exists in this set.'%name) + self.preferences.append(pref) + self.defaults[name] = default + + def option_parser(self, user_defaults=None, usage='', gui_mode=False): + from calibre.utils.config import OptionParser + parser = OptionParser(usage, gui_mode=gui_mode) + groups = defaultdict(lambda : parser) + for group, desc in self.groups.items(): + groups[group] = parser.add_option_group(group.upper(), desc) + + for pref in self.preferences: + if not pref.switches: + continue + g = groups[pref.group] + action = pref.action + if action is None: + action = 'store' + if pref.default is True or pref.default is False: + action = 'store_' + ('false' if pref.default else 'true') + args = dict( + dest=pref.name, + help=pref.help, + metavar=pref.metavar, + type=pref.type, + choices=pref.choices, + default=getattr(user_defaults, pref.name, pref.default), + action=action, + ) + g.add_option(*pref.switches, **args) + + + return parser + + def get_override_section(self, src): + match = self.OVERRIDE_PAT.search(src) + if match: + return match.group() + return '' + + def parse_string(self, src): + options = {'cPickle':cPickle} + if src is not None: + try: + if not isinstance(src, unicode): + src = src.decode('utf-8') + exec src in options + except: + print 'Failed to parse options string:' + print repr(src) + traceback.print_exc() + opts = OptionValues() + for pref in self.preferences: + val = options.get(pref.name, pref.default) + formatter = __builtins__.get(pref.type, None) + if callable(formatter): + val = formatter(val) + setattr(opts, pref.name, val) + + return opts + + def render_group(self, name, desc, opts): + prefs = [pref for pref in self.preferences if pref.group == name] + lines = ['### Begin group: %s'%(name if name else 'DEFAULT')] + if desc: + lines += map(lambda x: '# '+x, desc.split('\n')) + lines.append(' ') + for pref in prefs: + lines.append('# '+pref.name.replace('_', ' ')) + if pref.help: + lines += map(lambda x: '# ' + x, pref.help.split('\n')) + lines.append('%s = %s'%(pref.name, + self.serialize_opt(getattr(opts, pref.name, pref.default)))) + lines.append(' ') + return '\n'.join(lines) + + def serialize_opt(self, val): + if val is val is True or val is False or val is None or \ + isinstance(val, (int, float, long, basestring)): + return repr(val) + from PyQt4.QtCore import QString + if isinstance(val, QString): + return repr(unicode(val)) + pickle = cPickle.dumps(val, -1) + return 'cPickle.loads(%s)'%repr(pickle) + + def serialize(self, opts): + src = '# %s\n\n'%(self.description.replace('\n', '\n# ')) + groups = [self.render_group(name, self.groups.get(name, ''), opts) \ + for name in [None] + self.group_list] + return src + '\n\n'.join(groups) + +class ConfigInterface(object): + + def __init__(self, description): + self.option_set = OptionSet(description=description) + self.add_opt = self.option_set.add_opt + self.add_group = self.option_set.add_group + self.remove_opt = self.remove = self.option_set.remove_opt + self.parse_string = self.option_set.parse_string + self.get_option = self.option_set.get_option + self.preferences = self.option_set.preferences + + def update(self, other): + self.option_set.update(other.option_set) + + def option_parser(self, usage='', gui_mode=False): + return self.option_set.option_parser(user_defaults=self.parse(), + usage=usage, gui_mode=gui_mode) + + def smart_update(self, opts1, opts2): + self.option_set.smart_update(opts1, opts2) + + +class Config(ConfigInterface): + ''' + A file based configuration. + ''' + + def __init__(self, basename, description=''): + ConfigInterface.__init__(self, description) + self.config_file_path = os.path.join(config_dir, basename+'.py') + + + def parse(self): + src = '' + if os.path.exists(self.config_file_path): + try: + with ExclusiveFile(self.config_file_path) as f: + try: + src = f.read().decode('utf-8') + except ValueError: + print "Failed to parse", self.config_file_path + traceback.print_exc() + except LockError: + raise IOError('Could not lock config file: %s'%self.config_file_path) + return self.option_set.parse_string(src) + + def as_string(self): + if not os.path.exists(self.config_file_path): + return '' + try: + with ExclusiveFile(self.config_file_path) as f: + return f.read().decode('utf-8') + except LockError: + raise IOError('Could not lock config file: %s'%self.config_file_path) + + def set(self, name, val): + if not self.option_set.has_option(name): + raise ValueError('The option %s is not defined.'%name) + try: + if not os.path.exists(config_dir): + make_config_dir() + with ExclusiveFile(self.config_file_path) as f: + src = f.read() + opts = self.option_set.parse_string(src) + setattr(opts, name, val) + footer = self.option_set.get_override_section(src) + src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' + f.seek(0) + f.truncate() + if isinstance(src, unicode): + src = src.encode('utf-8') + f.write(src) + except LockError: + raise IOError('Could not lock config file: %s'%self.config_file_path) + +class StringConfig(ConfigInterface): + ''' + A string based configuration + ''' + + def __init__(self, src, description=''): + ConfigInterface.__init__(self, description) + self.src = src + + def parse(self): + return self.option_set.parse_string(self.src) + + def set(self, name, val): + if not self.option_set.has_option(name): + raise ValueError('The option %s is not defined.'%name) + opts = self.option_set.parse_string(self.src) + setattr(opts, name, val) + footer = self.option_set.get_override_section(self.src) + self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' + +class ConfigProxy(object): + ''' + A Proxy to minimize file reads for widely used config settings + ''' + + def __init__(self, config): + self.__config = config + self.__opts = None + + @property + def defaults(self): + return self.__config.option_set.defaults + + def refresh(self): + self.__opts = self.__config.parse() + + def __getitem__(self, key): + return self.get(key) + + def __setitem__(self, key, val): + return self.set(key, val) + + def get(self, key): + if self.__opts is None: + self.refresh() + return getattr(self.__opts, key) + + def set(self, key, val): + if self.__opts is None: + self.refresh() + setattr(self.__opts, key, val) + return self.__config.set(key, val) + + def help(self, key): + return self.__config.get_option(key).help + + + +def _prefs(): + c = Config('global', 'calibre wide preferences') + c.add_opt('database_path', + default=os.path.expanduser('~/library1.db'), + help=_('Path to the database in which books are stored')) + c.add_opt('filename_pattern', default=ur'(?P<title>.+) - (?P<author>[^_]+)', + help=_('Pattern to guess metadata from filenames')) + c.add_opt('isbndb_com_key', default='', + help=_('Access key for isbndb.com')) + c.add_opt('network_timeout', default=5, + help=_('Default timeout for network operations (seconds)')) + c.add_opt('library_path', default=None, + help=_('Path to directory in which your library of books is stored')) + c.add_opt('language', default=None, + help=_('The language in which to display the user interface')) + c.add_opt('output_format', default='EPUB', + help=_('The default output format for ebook conversions.')) + c.add_opt('input_format_order', default=['EPUB', 'MOBI', 'LIT', 'PRC', + 'FB2', 'HTML', 'HTM', 'XHTM', 'SHTML', 'XHTML', 'ZIP', 'ODT', 'RTF', 'PDF', + 'TXT'], + help=_('Ordered list of formats to prefer for input.')) + c.add_opt('read_file_metadata', default=True, + help=_('Read metadata from files')) + c.add_opt('worker_process_priority', default='normal', + help=_('The priority of worker processes. A higher priority ' + 'means they run faster and consume more resources. ' + 'Most tasks like conversion/news download/adding books/etc. ' + 'are affected by this setting.')) + c.add_opt('swap_author_names', default=False, + help=_('Swap author first and last names when reading metadata')) + c.add_opt('add_formats_to_existing', default=False, + help=_('Add new formats to existing book records')) + c.add_opt('installation_uuid', default=None, help='Installation UUID') + c.add_opt('new_book_tags', default=[], help=_('Tags to apply to books added to the library')) + + # these are here instead of the gui preferences because calibredb and + # calibre server can execute searches + c.add_opt('saved_searches', default={}, help=_('List of named saved searches')) + c.add_opt('user_categories', default={}, help=_('User-created tag browser categories')) + c.add_opt('manage_device_metadata', default='manual', + help=_('How and when calibre updates metadata on the device.')) + c.add_opt('limit_search_columns', default=False, + help=_('When searching for text without using lookup ' + 'prefixes, as for example, Red instead of title:Red, ' + 'limit the columns searched to those named below.')) + c.add_opt('limit_search_columns_to', + default=['title', 'authors', 'tags', 'series', 'publisher'], + help=_('Choose columns to be searched when not using prefixes, ' + 'as for example, when searching for Redd instead of ' + 'title:Red. Enter a list of search/lookup names ' + 'separated by commas. Only takes effect if you set the option ' + 'to limit search columns above.')) + + c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') + return c + +prefs = ConfigProxy(_prefs()) +if prefs['installation_uuid'] is None: + import uuid + prefs['installation_uuid'] = str(uuid.uuid4()) + +# Read tweaks +def read_raw_tweaks(): + make_config_dir() + default_tweaks = P('default_tweaks.py', data=True, + allow_user_override=False) + tweaks_file = os.path.join(config_dir, 'tweaks.py') + if not os.path.exists(tweaks_file): + with open(tweaks_file, 'wb') as f: + f.write(default_tweaks) + with open(tweaks_file, 'rb') as f: + return default_tweaks, f.read() + +def read_tweaks(): + default_tweaks, tweaks = read_raw_tweaks() + l, g = {}, {} + try: + exec tweaks in g, l + except: + import traceback + print 'Failed to load custom tweaks file' + traceback.print_exc() + dl, dg = {}, {} + exec default_tweaks in dg, dl + dl.update(l) + return dl + +def write_tweaks(raw): + make_config_dir() + tweaks_file = os.path.join(config_dir, 'tweaks.py') + with open(tweaks_file, 'wb') as f: + f.write(raw) + + +tweaks = read_tweaks() + + diff --git a/src/calibre/utils/icu.py b/src/calibre/utils/icu.py index f17ff1b17f..d5bef449c4 100644 --- a/src/calibre/utils/icu.py +++ b/src/calibre/utils/icu.py @@ -10,7 +10,7 @@ import sys from functools import partial from calibre.constants import plugins -from calibre.utils.config import tweaks +from calibre.utils.config_base import tweaks _icu = _collator = None _locale = None diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index f676b99e43..533fd03457 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -24,7 +24,7 @@ def available_translations(): def get_lang(): 'Try to figure out what language to display the interface in' - from calibre.utils.config import prefs + from calibre.utils.config_base import prefs lang = prefs['language'] lang = os.environ.get('CALIBRE_OVERRIDE_LANG', lang) if lang is not None: From 5884c1b21e9c0721774a53f946c28eb86afc5cce Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Wed, 20 Apr 2011 13:46:02 -0600 Subject: [PATCH 24/32] ... --- setup/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup/__init__.py b/setup/__init__.py index 9e62fb377d..61bafd2282 100644 --- a/setup/__init__.py +++ b/setup/__init__.py @@ -24,8 +24,10 @@ def initialize_constants(): global __version__, __appname__, modules, functions, basenames, scripts src = open('src/calibre/constants.py', 'rb').read() - __version__ = re.search(r'__version__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1) - __appname__ = re.search(r'__appname__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1) + nv = re.search(r'numeric_version\s+=\s+\((\d+), (\d+), (\d+)\)', src) + __version__ = '%s.%s.%s'%(nv.group(1), nv.group(2), nv.group(3)) + __appname__ = re.search(r'__appname__\s+=\s+(u{0,1})[\'"]([^\'"]+)[\'"]', + src).group(2) epsrc = re.compile(r'entry_points = (\{.*?\})', re.DOTALL).\ search(open('src/calibre/linux.py', 'rb').read()).group(1) entry_points = eval(epsrc, {'__appname__': __appname__}) From 303a7c6b5c0ded507fb8685ecb2989f0ab20b945 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Wed, 20 Apr 2011 13:51:42 -0600 Subject: [PATCH 25/32] ... --- src/calibre/utils/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 87682c9792..8b23cf3071 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -16,13 +16,13 @@ from calibre.constants import (config_dir, CONFIG_DIR_MODE, __appname__, from calibre.utils.lock import ExclusiveFile from calibre.utils.config_base import (make_config_dir, Option, OptionValues, OptionSet, ConfigInterface, Config, prefs, StringConfig, ConfigProxy, - read_raw_tweaks, read_tweaks, write_tweaks, tweaks) + read_raw_tweaks, read_tweaks, write_tweaks, tweaks, plugin_dir) if False: # Make pyflakes happy Config, ConfigProxy, Option, OptionValues, StringConfig OptionSet, ConfigInterface, read_tweaks, write_tweaks - read_raw_tweaks, tweaks + read_raw_tweaks, tweaks, plugin_dir test_eight_code = tweaks.get('test_eight_code', False) From 7fc7478c979751606da53b7f9d5fbed0225f971d Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Wed, 20 Apr 2011 14:06:56 -0600 Subject: [PATCH 26/32] Delay load calibre C extensions. Worker launch is now under 0.07 seconds (only about 3 times the time taken to launch bare python) --- src/calibre/constants.py | 64 +++++++++++++++++++++++----------- src/calibre/devices/scanner.py | 2 +- src/calibre/gui2/main.py | 6 ++-- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 62612d9c66..a15ffbf967 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -12,7 +12,7 @@ __author__ = u"Kovid Goyal <kovid@kovidgoyal.net>" Various run time constants. ''' -import sys, locale, codecs, os, importlib +import sys, locale, codecs, os, importlib, collections _tc = None def terminal_controller(): @@ -52,15 +52,12 @@ def debug(): DEBUG = True # plugins {{{ -plugins = None -if plugins is None: - # Load plugins - def load_plugins(): - plugins = {} - plugin_path = sys.extensions_location - sys.path.insert(0, plugin_path) - for plugin in [ +class Plugins(collections.Mapping): + + def __init__(self): + self._plugins = {} + plugins = [ 'pictureflow', 'lzx', 'msdes', @@ -74,19 +71,44 @@ if plugins is None: 'chm_extra', 'icu', 'speedup', - ] + \ - (['winutil'] if iswindows else []) + \ - (['usbobserver'] if isosx else []): - try: - p, err = importlib.import_module(plugin), '' - except Exception as err: - p = None - err = str(err) - plugins[plugin] = (p, err) - sys.path.remove(plugin_path) - return plugins + ] + if iswindows: + plugins.append('winutil') + if isosx: + plugins.append(['usbobserver']) + self.plugins = frozenset(plugins) - plugins = load_plugins() + def load_plugin(self, name): + if name in self._plugins: + return + sys.path.insert(0, sys.extensions_location) + try: + p, err = importlib.import_module(name), '' + except Exception as err: + p = None + err = str(err) + self._plugins[name] = (p, err) + sys.path.remove(sys.extensions_location) + + def __iter__(self): + return iter(self.plugins) + + def __len__(self): + return len(self.plugins) + + def __contains__(self, name): + return name in self.plugins + + def __getitem__(self, name): + if name not in self.plugins: + raise KeyError('No plugin named %r'%name) + self.load_plugin(name) + return self._plugins[name] + + +plugins = None +if plugins is None: + plugins = Plugins() # }}} # config_dir {{{ diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index c63eada0c8..9b729a3561 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -8,7 +8,7 @@ manner. import sys, os, re from threading import RLock -from calibre import iswindows, isosx, plugins, islinux +from calibre.constants import iswindows, isosx, plugins, islinux osx_scanner = win_scanner = linux_scanner = None diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index c67ec8c2b4..ee18d8e9ca 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -19,6 +19,9 @@ from calibre.utils.config import prefs, dynamic from calibre.library.database2 import LibraryDatabase2 from calibre.library.sqlite import sqlite, DatabaseException +if iswindows: + winutil = plugins['winutil'][0] + def option_parser(): parser = _option_parser('''\ %prog [opts] [path_to_ebook] @@ -80,8 +83,7 @@ def get_library_path(parent=None): if library_path is None: # Need to migrate to new database layout base = os.path.expanduser('~') if iswindows: - base = plugins['winutil'][0].special_folder_path( - plugins['winutil'][0].CSIDL_PERSONAL) + base = winutil.special_folder_path(winutil.CSIDL_PERSONAL) if not base or not os.path.exists(base): from PyQt4.Qt import QDir base = unicode(QDir.homePath()).replace('/', os.sep) From 7b2ea64d43bcce5281ea270b6f68a3e2c28dfe93 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Wed, 20 Apr 2011 14:11:48 -0600 Subject: [PATCH 27/32] ... --- src/calibre/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/constants.py b/src/calibre/constants.py index a15ffbf967..e673f8878a 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -75,7 +75,7 @@ class Plugins(collections.Mapping): if iswindows: plugins.append('winutil') if isosx: - plugins.append(['usbobserver']) + plugins.append('usbobserver') self.plugins = frozenset(plugins) def load_plugin(self, name): From 6502429c6de42504beaef7c6c2ee10d6dfb5bfc0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Wed, 20 Apr 2011 15:35:40 -0600 Subject: [PATCH 28/32] Fix Il Sole 24 Ore --- recipes/ilsole24ore.recipe | 98 ++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 52 deletions(-) diff --git a/recipes/ilsole24ore.recipe b/recipes/ilsole24ore.recipe index 920c703222..0cf1ddc6bf 100644 --- a/recipes/ilsole24ore.recipe +++ b/recipes/ilsole24ore.recipe @@ -1,71 +1,65 @@ -#!/usr/bin/env python -__license__ = 'GPL v3' -__author__ = 'Lorenzo Vigentini & Edwin van Maastrigt' -__copyright__ = '2009, Lorenzo Vigentini <l.vigentini at gmail.com> and Edwin van Maastrigt <evanmaastrigt at gmail.com>' -__description__ = 'Financial news daily paper - v1.02 (30, January 2010)' +__author__ = 'Marco Saraceno' +__copyright__ = '2010, Marco Saraceno <marcosaraceno at gmail.com>' +description = 'Italian daily newspaper - v 1.1 (Mar14,2011)' ''' -http://www.ilsole24ore.com/ +http://www.ilsole24ore.com ''' from calibre.web.feeds.news import BasicNewsRecipe +class IlSole24Ore(BasicNewsRecipe): + __author__ = 'Marco Saraceno' + description = 'Italian financial daily newspaper' -class ilsole24Ore(BasicNewsRecipe): - author = 'Lorenzo Vigentini & Edwin van Maastrigt' - description = 'Financial news daily paper' - - cover_url = 'http://www.ilsole24ore.com/img2007/print_header.gif' - - title = u'il Sole 24 Ore New' - publisher = 'italiaNews' - category = 'News, finance, economy, politics' + cover_url = 'http://www.shopping24.ilsole24ore.com/ProductRelated/rds/img/logo_sole.gif' + title = u'Il Sole 24 Ore' + publisher = 'Gruppo editoriale GRUPPO 24ORE' + category = 'News, politics, culture, economy, financial, Italian' language = 'it' timefmt = '[%a, %d %b, %Y]' oldest_article = 2 - max_articles_per_feed = 50 + max_articles_per_feed = 100 use_embedded_content = False + extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }' + + + remove_tags = [ + dict(name='div', attrs={'class':['header','titolo']}), + dict(name='table', attrs={'class':['footer1024','footerdown']}), + ] - remove_javascript = True - no_stylesheets = True def get_article_url(self, article): - return article.get('id', article.get('guid', None)) + link = article.get('link', None) + if link is None: + return article + if link.split('/')[-1]=="story01.htm": + link=link.split('/')[-2] + a=['0B','0C','0D','0E','0F','0G','0N' ,'0L0S','0A'] + b=['.' ,'/' ,'?' ,'-' ,'=' ,'&' ,'.com','www.','0'] + for i in range(0,len(a)): + link=link.replace(a[i],b[i]) + link="http://"+link + return link + + feeds = [ + (u'Notizie Italia', u'http://www.ilsole24ore.com/rss/notizie/italia.xml'), + (u'Notizie Europa', u'http://www.ilsole24ore.com/rss/notizie/europa.xml'), + (u'Notizie USA', u'http://www.ilsole24ore.com/rss/notizie/usa.xml'), + (u'Notizie Americhe', u'http://www.ilsole24ore.com/rss/notizie/americhe.xml'), + (u'Notizie Medio Oriente e Africa', u'http://www.ilsole24ore.com/rss/notizie/medio-oriente-e-africa.xml'), + (u'Notizie Asia e Oceania', u'http://www.ilsole24ore.com/rss/notizie/asia-e-oceania.xml'), + (u'Commenti', u'http://www.ilsole24ore.com/rss/commenti-e-idee.xml'), + (u'Norme e tributi', u'http://www.ilsole24ore.com/rss/norme-e-tributi.xml'), + (u'Finanza', u'http://www.ilsole24ore.com/rss/finanza-e-mercati.xml'), + (u'Economia', u'http://www.ilsole24ore.com/rss/economia.xml'), + (u'Tecnologia', u'http://www.ilsole24ore.com/rss/tecnologie.xml'), + (u'Cultura', u'http://www.ilsole24ore.com/rss/cultura.xml'), + ] def print_version(self, url): - link, sep, params = url.rpartition('?') - if link is None: - return link.replace('_1.php', '_php') - return link.replace('.shtml', '_PRN.shtml') - - keep_only_tags = [ - dict(name='div', attrs={'class':'txt'}) - ] -# remove_tags = [dict(name='br')] - - feeds = [ - (u'Prima pagina', u'http://www.ilsole24ore.com/rss/primapagina.xml'), - (u'Norme e tributi', u'http://www.ilsole24ore.com/rss/norme-tributi.xml'), - (u'Finanza e mercati', u'http://www.ilsole24ore.com/rss/finanza-mercati.xml'), - (u'Economia e lavoro', u'http://www.ilsole24ore.com/rss/economia-lavoro.xml'), - (u'Italia', u'http://www.ilsole24ore.com/rss/italia.xml'), - (u'Mondo', u'http://www.ilsole24ore.com/rss/mondo.xml'), - (u'Tecnologia e business', u'http://www.ilsole24ore.com/rss/tecnologia-business.xml'), - (u'Cultura e tempo libero', u'http://www.ilsole24ore.com/rss/tempolibero-cultura.xml'), - (u'Sport', u'http://www.ilsole24ore.com/rss/sport.xml'), - (u'Professionisti 24', u'http://www.ilsole24ore.com/rss/prof_home.xml'), - (u'Ambiente e Sicurezza',u'http://www.ilsole24ore.com/rss/prof_as.xml') - ] - - extra_css = ''' - html, body, table, tr, td, h1, h2, h3, h4, h5, h6, p, a, span, br, img {margin:0;padding:0;border:0;font-size:12px;font-family:"Georgia","Times New Roman";} - .linkHighlight {color:#0292c6;} - .txt {border-bottom:1px solid #7c7c7c;padding-bottom:20px};text-align:justify;font-family:"serif"} - .txt p {line-height:18px;} - .txt span {line-height:22px;} - .title h3 {color:#7b7b7b;} - .title h4 {color:#08526e;font-size:26px;font-family:"Times New Roman";font-weight:normal;} - ''' + return url.replace('.shtml', '_PRN.shtml') From f05d6d063fb22ed17cd4f890f1797130c5e00019 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Wed, 20 Apr 2011 16:51:59 -0600 Subject: [PATCH 29/32] ... --- resources/template-functions.json | 5 +++-- src/calibre/constants.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/resources/template-functions.json b/resources/template-functions.json index c19627c6c7..cf858c7691 100644 --- a/resources/template-functions.json +++ b/resources/template-functions.json @@ -5,6 +5,7 @@ "strcat": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n res = ''\n for i in range(0, len(args)):\n res += args[i]\n return res\n", "substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n", "ifempty": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):\n if val:\n return val\n else:\n return value_if_empty\n", + "booksize": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.book_size is not None:\n try:\n return str(mi.book_size)\n except:\n pass\n return ''\n", "select": "def evaluate(self, formatter, kwargs, mi, locals, val, key):\n if not val:\n return ''\n vals = [v.strip() for v in val.split(',')]\n for v in vals:\n if v.startswith(key+':'):\n return v[len(key)+1:]\n return ''\n", "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n", "subtract": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x - y)\n", @@ -25,9 +26,9 @@ "capitalize": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return capitalize(val)\n", "count": "def evaluate(self, formatter, kwargs, mi, locals, val, sep):\n return unicode(len(val.split(sep)))\n", "lowercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.lower()\n", - "assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n", - "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n", "strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n", + "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n", + "assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n", "raw_field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return unicode(getattr(mi, name, None))\n", "cmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n x = float(x if x else 0)\n y = float(y if y else 0)\n if x < y:\n return lt\n if x == y:\n return eq\n return gt\n" } \ No newline at end of file diff --git a/src/calibre/constants.py b/src/calibre/constants.py index e673f8878a..28dbcc4299 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -22,11 +22,11 @@ def terminal_controller(): _tc = TerminalController(sys.stdout) return _tc - -iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower() -isosx = 'darwin' in sys.platform.lower() -isnewosx = isosx and getattr(sys, 'new_app_bundle', False) -isfreebsd = 'freebsd' in sys.platform.lower() +_plat = sys.platform.lower() +iswindows = 'win32' in _plat or 'win64' in _plat +isosx = 'darwin' in _plat +isnewosx = isosx and getattr(sys, 'new_app_bundle', False) +isfreebsd = 'freebsd' in _plat islinux = not(iswindows or isosx or isfreebsd) isfrozen = hasattr(sys, 'frozen') isunix = isosx or islinux From f15e78aa3eac06ade474078928a5b581cad30ecd Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Wed, 20 Apr 2011 17:03:49 -0600 Subject: [PATCH 30/32] Fix Handelsblatt --- recipes/handelsblatt.recipe | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/recipes/handelsblatt.recipe b/recipes/handelsblatt.recipe index 945dac0560..056fcfb26b 100644 --- a/recipes/handelsblatt.recipe +++ b/recipes/handelsblatt.recipe @@ -1,4 +1,3 @@ - from calibre.web.feeds.news import BasicNewsRecipe class Handelsblatt(BasicNewsRecipe): @@ -7,14 +6,11 @@ class Handelsblatt(BasicNewsRecipe): oldest_article = 7 max_articles_per_feed = 100 no_stylesheets = True - cover_url = 'http://www.handelsblatt.com/images/logo/logo_handelsblatt.com.png' +# cover_url = 'http://www.handelsblatt.com/images/logo/logo_handelsblatt.com.png' language = 'de' - # keep_only_tags = [] - keep_only_tags = (dict(name = 'div', attrs = {'class': ['hcf-detail-abstract hcf-teaser ajaxify','hcf-detail','hcf-author-wrapper']})) - # keep_only_tags.append(dict(name = 'div', attrs = {'id': 'fullText'})) - remove_tags = [dict(name='img', attrs = {'src': 'http://www.handelsblatt.com/images/icon/loading.gif'}) - ,dict(name='ul' , attrs={'class':['hcf-detail-tools']}) - ] + + remove_tags_before = dict(attrs={'class':'hcf-overline'}) + remove_tags_after = dict(attrs={'class':'hcf-footer'}) feeds = [ (u'Handelsblatt Exklusiv',u'http://www.handelsblatt.com/rss/exklusiv'), @@ -28,17 +24,16 @@ class Handelsblatt(BasicNewsRecipe): (u'Handelsblatt Magazin',u'http://www.handelsblatt.com/rss/magazin/'), (u'Handelsblatt Weblogs',u'http://www.handelsblatt.com/rss/blogs') ] + extra_css = ''' - .hcf-headline {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:x-large;} - .hcf-overline {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:x-large;} - .hcf-exclusive {font-family:Arial,Helvetica,sans-serif; font-style:italic;font-weight:bold; margin-right:5pt;} - p{font-family:Arial,Helvetica,sans-serif;} - .hcf-location-mark{font-weight:bold; margin-right:5pt;} - .MsoNormal{font-family:Helvetica,Arial,sans-serif;} - .hcf-author-wrapper{font-style:italic;} - .hcf-article-date{font-size:x-small;} - .hcf-caption {font-style:italic;font-size:small;} - img {align:left;} + h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;} + h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} + p{font-family:Arial,Helvetica,sans-serif;font-size:small;} + body{font-family:Helvetica,Arial,sans-serif;font-size:small;} ''' - + def print_version(self, url): + url = url.split('/') + url[-1] = 'v_detail_tab_print,'+url[-1] + url = '/'.join(url) + return url From f48de860fa1a83abd5873e1327e18cf9ae35deaa Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Wed, 20 Apr 2011 20:05:41 -0600 Subject: [PATCH 31/32] ... --- src/calibre/gui2/metadata/bulk_download2.py | 53 +++++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/calibre/gui2/metadata/bulk_download2.py b/src/calibre/gui2/metadata/bulk_download2.py index 2bbb177e14..4b59ce68e9 100644 --- a/src/calibre/gui2/metadata/bulk_download2.py +++ b/src/calibre/gui2/metadata/bulk_download2.py @@ -242,7 +242,7 @@ def apply_metadata(job, gui, q, result): q.finished.disconnect() if result != q.Accepted: return - id_map, failed_ids, failed_covers, title_map = job.result + id_map, failed_ids, failed_covers, title_map, all_failed = job.result id_map = dict([(k, v) for k, v in id_map.iteritems() if k not in failed_ids]) if not id_map: @@ -279,31 +279,39 @@ def apply_metadata(job, gui, q, result): def proceed(gui, job): gui.status_bar.show_message(_('Metadata download completed'), 3000) - id_map, failed_ids, failed_covers, title_map = job.result - fmsg = det_msg = '' - if failed_ids or failed_covers: - fmsg = '<p>'+_('Could not download metadata and/or covers for %d of the books. Click' - ' "Show details" to see which books.')%len(failed_ids) - det_msg = [] - for i in failed_ids | failed_covers: - title = title_map[i] - if i in failed_ids: - title += (' ' + _('(Failed metadata)')) - if i in failed_covers: - title += (' ' + _('(Failed cover)')) - det_msg.append(title) - msg = '<p>' + _('Finished downloading metadata for <b>%d book(s)</b>. ' - 'Proceed with updating the metadata in your library?')%len(id_map) - q = MessageBox(MessageBox.QUESTION, _('Download complete'), - msg + fmsg, det_msg='\n'.join(det_msg), show_copy_button=bool(failed_ids), - parent=gui) + id_map, failed_ids, failed_covers, title_map, all_failed = job.result + if all_failed: + q = error_dialog(gui, _('Download failed'), + _('Failed to download metadata or covers for any of the %d' + ' book(s).') % len(id_map)) + else: + fmsg = det_msg = '' + if failed_ids or failed_covers: + fmsg = '<p>'+_('Could not download metadata and/or covers for %d of the books. Click' + ' "Show details" to see which books.')%len(failed_ids) + det_msg = [] + for i in failed_ids | failed_covers: + title = title_map[i] + if i in failed_ids: + title += (' ' + _('(Failed metadata)')) + if i in failed_covers: + title += (' ' + _('(Failed cover)')) + det_msg.append(title) + msg = '<p>' + _('Finished downloading metadata for <b>%d book(s)</b>. ' + 'Proceed with updating the metadata in your library?')%len(id_map) + q = MessageBox(MessageBox.QUESTION, _('Download complete'), + msg + fmsg, det_msg='\n'.join(det_msg), show_copy_button=bool(failed_ids), + parent=gui) + q.finished.connect(partial(apply_metadata, job, gui, q)) + q.vlb = q.bb.addButton(_('View log'), q.bb.ActionRole) q.vlb.setIcon(QIcon(I('debug.png'))) q.vlb.clicked.connect(partial(view_log, job, q)) q.det_msg_toggle.setVisible(bool(failed_ids | failed_covers)) q.setModal(False) + if all_failed: + q.det_msg_toggle.setVisible(False) q.show() - q.finished.connect(partial(apply_metadata, job, gui, q)) # }}} @@ -336,6 +344,7 @@ def download(ids, db, do_identify, covers, title_map = {} ans = {} count = 0 + all_failed = True for i, mi in izip(ids, metadata): if abort.is_set(): log.error('Aborting...') @@ -350,6 +359,7 @@ def download(ids, db, do_identify, covers, except: pass if results: + all_failed = False mi = merge_result(mi, results[0]) identifiers = mi.identifiers if not mi.is_null('rating'): @@ -367,6 +377,7 @@ def download(ids, db, do_identify, covers, with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f: f.write(cdata[-1]) mi.cover = f.name + all_failed = False else: failed_covers.add(i) ans[i] = mi @@ -374,7 +385,7 @@ def download(ids, db, do_identify, covers, notifications.put((count/len(ids), _('Downloaded %d of %d')%(count, len(ids)))) log('Download complete, with %d failures'%len(failed_ids)) - return (ans, failed_ids, failed_covers, title_map) + return (ans, failed_ids, failed_covers, title_map, all_failed) From ac8df2f09962c58f2d7ed8c8696967203c132efb Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Wed, 20 Apr 2011 20:09:47 -0600 Subject: [PATCH 32/32] ... --- src/calibre/gui2/metadata/bulk_download2.py | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/metadata/bulk_download2.py b/src/calibre/gui2/metadata/bulk_download2.py index 4b59ce68e9..82804b9c96 100644 --- a/src/calibre/gui2/metadata/bulk_download2.py +++ b/src/calibre/gui2/metadata/bulk_download2.py @@ -280,27 +280,29 @@ def apply_metadata(job, gui, q, result): def proceed(gui, job): gui.status_bar.show_message(_('Metadata download completed'), 3000) id_map, failed_ids, failed_covers, title_map, all_failed = job.result + det_msg = [] + for i in failed_ids | failed_covers: + title = title_map[i] + if i in failed_ids: + title += (' ' + _('(Failed metadata)')) + if i in failed_covers: + title += (' ' + _('(Failed cover)')) + det_msg.append(title) + det_msg = '\n'.join(det_msg) + if all_failed: q = error_dialog(gui, _('Download failed'), _('Failed to download metadata or covers for any of the %d' - ' book(s).') % len(id_map)) + ' book(s).') % len(id_map), det_msg=det_msg) else: fmsg = det_msg = '' if failed_ids or failed_covers: fmsg = '<p>'+_('Could not download metadata and/or covers for %d of the books. Click' ' "Show details" to see which books.')%len(failed_ids) - det_msg = [] - for i in failed_ids | failed_covers: - title = title_map[i] - if i in failed_ids: - title += (' ' + _('(Failed metadata)')) - if i in failed_covers: - title += (' ' + _('(Failed cover)')) - det_msg.append(title) msg = '<p>' + _('Finished downloading metadata for <b>%d book(s)</b>. ' 'Proceed with updating the metadata in your library?')%len(id_map) q = MessageBox(MessageBox.QUESTION, _('Download complete'), - msg + fmsg, det_msg='\n'.join(det_msg), show_copy_button=bool(failed_ids), + msg + fmsg, det_msg=det_msg, show_copy_button=bool(failed_ids), parent=gui) q.finished.connect(partial(apply_metadata, job, gui, q)) @@ -309,8 +311,6 @@ def proceed(gui, job): q.vlb.clicked.connect(partial(view_log, job, q)) q.det_msg_toggle.setVisible(bool(failed_ids | failed_covers)) q.setModal(False) - if all_failed: - q.det_msg_toggle.setVisible(False) q.show() # }}}