From 2ae0cc048d489204161f29e825485920c0604662 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 27 Jun 2011 17:18:51 +0100 Subject: [PATCH 01/18] Make get_categories respect a search restriction that finds no books. This is an incompatible change, as [] used to mean no restriction and now it means no books. Tested with tag browser and content server. --- src/calibre/library/database2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 8b4ad47284..9229d44cac 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1442,7 +1442,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): raise ValueError('sort ' + sort + ' not a valid value') self.books_list_filter.change([] if not ids else ids) - id_filter = None if not ids else frozenset(ids) + id_filter = None if ids is None else frozenset(ids) tb_cats = self.field_metadata tcategories = {} @@ -1520,7 +1520,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): rating_dex = self.FIELD_MAP['rating'] tag_class = LibraryDatabase2.TCat_Tag for book in self.data.iterall(): - if id_filter and book[id_dex] not in id_filter: + if id_filter is not None and book[id_dex] not in id_filter: continue rating = book[rating_dex] # We kept track of all possible category field_map positions above From 1441bce41c71f87ee9a9c2bf818a7322e79c5ffd Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 27 Jun 2011 17:19:31 +0100 Subject: [PATCH 02/18] Remove spacer that shouldn't be there. Add 2-criteria sorting for series. --- src/calibre/gui2/dialogs/quickview.py | 21 +++++++++++++++++---- src/calibre/gui2/dialogs/quickview.ui | 13 ------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/dialogs/quickview.py b/src/calibre/gui2/dialogs/quickview.py index 3a69368730..ec8e6b6bc7 100644 --- a/src/calibre/gui2/dialogs/quickview.py +++ b/src/calibre/gui2/dialogs/quickview.py @@ -18,16 +18,29 @@ class TableItem(QTableWidgetItem): A QTableWidgetItem that sorts on a separate string and uses ICU rules ''' - def __init__(self, val, sort): + def __init__(self, val, sort, idx=0): self.sort = sort + self.sort_idx = idx QTableWidgetItem.__init__(self, val) self.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable) def __ge__(self, other): - return sort_key(self.sort) >= sort_key(other.sort) + l = sort_key(self.sort) + r = sort_key(other.sort) + if l > r: + return 1 + if l == r: + return self.sort_idx >= other.sort_idx + return 0 def __lt__(self, other): - return sort_key(self.sort) < sort_key(other.sort) + l = sort_key(self.sort) + r = sort_key(other.sort) + if l < r: + return 1 + if l == r: + return self.sort_idx < other.sort_idx + return 0 class Quickview(QDialog, Ui_Quickview): @@ -185,7 +198,7 @@ class Quickview(QDialog, Ui_Quickview): series = mi.format_field('series')[1] if series is None: series = '' - a = TableItem(series, series) + a = TableItem(series, mi.series, mi.series_index) a.setToolTip(tt) self.books_table.setItem(row, 2, a) self.books_table.setRowHeight(row, self.books_table_row_height) diff --git a/src/calibre/gui2/dialogs/quickview.ui b/src/calibre/gui2/dialogs/quickview.ui index 2cdc7b7379..4b040e34d3 100644 --- a/src/calibre/gui2/dialogs/quickview.ui +++ b/src/calibre/gui2/dialogs/quickview.ui @@ -57,19 +57,6 @@ - - - - Qt::Vertical - - - - 0 - 0 - - - - From 2930304cd8eb549dfe6edb2a5983a461ba2c8393 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 27 Jun 2011 18:00:38 +0100 Subject: [PATCH 03/18] tag_view: Fix uses of py_name that should be category_key. --- src/calibre/gui2/tag_browser/model.py | 4 ++-- src/calibre/gui2/tag_browser/view.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 5589a1bcb4..e759783d7b 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -514,7 +514,7 @@ class TagsModel(QAbstractItemModel): # {{{ # }}} for category in self.category_nodes: - process_one_node(category, state_map.get(category.py_name, {})) + process_one_node(category, state_map.get(category.category_key, {})) # Drag'n Drop {{{ def mimeTypes(self): @@ -851,7 +851,7 @@ class TagsModel(QAbstractItemModel): # {{{ def index_for_category(self, name): for row, category in enumerate(self.category_nodes): - if category.py_name == name: + if category.category_key == name: return self.index(row, 0, QModelIndex()) def columnCount(self, parent): diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index 1fad4eb9a3..c833f7fa43 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -129,10 +129,10 @@ class TagsView(QTreeView): # {{{ expanded_categories = [] for row, category in enumerate(self._model.category_nodes): if self.isExpanded(self._model.index(row, 0, QModelIndex())): - expanded_categories.append(category.py_name) + expanded_categories.append(category.category_key) states = [c.tag.state for c in category.child_tags()] names = [(c.tag.name, c.tag.category) for c in category.child_tags()] - state_map[category.py_name] = dict(izip(names, states)) + state_map[category.category_key] = dict(izip(names, states)) return expanded_categories, state_map def reread_collapse_parameters(self): From 7f1905b779ce141df4943cc954d0e01f6281a79b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 27 Jun 2011 18:18:01 +0100 Subject: [PATCH 04/18] Make quickview survive changing the library. --- src/calibre/gui2/actions/show_quickview.py | 3 +++ src/calibre/gui2/dialogs/quickview.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/src/calibre/gui2/actions/show_quickview.py b/src/calibre/gui2/actions/show_quickview.py index 78352e6da8..4f7bbc0473 100644 --- a/src/calibre/gui2/actions/show_quickview.py +++ b/src/calibre/gui2/actions/show_quickview.py @@ -38,3 +38,6 @@ class ShowQuickviewAction(InterfaceAction): Quickview(self.gui, self.gui.library_view, index) self.current_instance.show() + def library_changed(self, db): + if self.current_instance and not self.current_instance.is_closed: + self.current_instance.set_database(db) diff --git a/src/calibre/gui2/dialogs/quickview.py b/src/calibre/gui2/dialogs/quickview.py index ec8e6b6bc7..30b68a7b7d 100644 --- a/src/calibre/gui2/dialogs/quickview.py +++ b/src/calibre/gui2/dialogs/quickview.py @@ -108,6 +108,15 @@ class Quickview(QDialog, Ui_Quickview): self.search_button.clicked.connect(self.do_search) view.model().new_bookdisplay_data.connect(self.book_was_changed) + def set_database(self, db): + self.db = db + self.items.blockSignals(True) + self.books_table.blockSignals(True) + self.items.clear() + self.books_table.setRowCount(0) + self.books_table.blockSignals(False) + self.items.blockSignals(False) + # search button def do_search(self): if self.last_search is not None: From d1d5ee7a3e022331f775ddf812d6fb72bef49cd0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 12:13:30 -0600 Subject: [PATCH 05/18] ... --- src/calibre/gui2/preferences/main.py | 1 - src/calibre/gui2/preferences/search.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py index 85a5fc018c..774b7f8958 100644 --- a/src/calibre/gui2/preferences/main.py +++ b/src/calibre/gui2/preferences/main.py @@ -357,7 +357,6 @@ class Preferences(QMainWindow): bytearray(self.saveGeometry())) if self.committed: self.gui.must_restart_before_config = self.must_restart - self.gui.tags_view.set_new_model() # in case columns changed self.gui.tags_view.recount() self.gui.create_device_menu() self.gui.set_device_menu_items_state(bool(self.gui.device_connected)) diff --git a/src/calibre/gui2/preferences/search.py b/src/calibre/gui2/preferences/search.py index 7bdb12ec55..c86de7f2a3 100644 --- a/src/calibre/gui2/preferences/search.py +++ b/src/calibre/gui2/preferences/search.py @@ -173,7 +173,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): def refresh_gui(self, gui): gui.set_highlight_only_button_icon() if self.muc_changed: - gui.tags_view.set_new_model() + gui.tags_view.recount() gui.search.search_as_you_type(config['search_as_you_type']) gui.search.do_search() From ef13b74d1a6fb459688fe3de3f9f679889983b9a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 12:48:01 -0600 Subject: [PATCH 06/18] Tag Browser: Do not allow methods to be called in a non GUI thread --- src/calibre/gui2/tag_browser/view.py | 30 +++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index c833f7fa43..ed1a597827 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -16,9 +16,10 @@ from PyQt4.Qt import (QItemDelegate, Qt, QTreeView, pyqtSignal, QSize, QIcon, from calibre.gui2.tag_browser.model import (TagTreeItem, TAG_SEARCH_STATES, TagsModel) -from calibre.gui2 import config, gprefs +from calibre.gui2 import config, gprefs, is_gui_thread from calibre.utils.search_query_parser import saved_searches from calibre.utils.icu import sort_key +from calibre.constants import DEBUG class TagDelegate(QItemDelegate): # {{{ @@ -125,6 +126,8 @@ class TagsView(QTreeView): # {{{ self.recount() def get_state(self): + if not is_gui_thread(): + return self.debug_threading() state_map = {} expanded_categories = [] for row, category in enumerate(self._model.category_nodes): @@ -136,9 +139,13 @@ class TagsView(QTreeView): # {{{ return expanded_categories, state_map def reread_collapse_parameters(self): + if not is_gui_thread(): + return self.debug_threading() self._model.reread_collapse_parameters(self.get_state()[1]) def set_database(self, db, tag_match, sort_by): + if not is_gui_thread(): + return self.debug_threading() self._model.set_database(db) self.pane_is_visible = True # because TagsModel.set_database did a recount @@ -165,6 +172,8 @@ class TagsView(QTreeView): # {{{ self.expanded.connect(self.item_expanded) def database_changed(self, event, ids): + if not is_gui_thread(): + return self.debug_threading() if self.refresh_signal_processed: self.refresh_signal_processed = False self.refresh_required.emit() @@ -191,6 +200,8 @@ class TagsView(QTreeView): # {{{ pass def set_search_restriction(self, s): + if not is_gui_thread(): + return self.debug_threading() s = s if s else None self._model.set_search_restriction(s) @@ -217,6 +228,8 @@ class TagsView(QTreeView): # {{{ set_to: if None, advance the state. Otherwise must be one of the values in TAG_SEARCH_STATES ''' + if not is_gui_thread(): + return self.debug_threading() modifiers = int(QApplication.keyboardModifiers()) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) if self._model.toggle(index, exclusive, set_to=set_to): @@ -529,11 +542,21 @@ class TagsView(QTreeView): # {{{ fm_src['display'].get('make_category', False)))): self.setDropIndicatorShown(True) + def debug_threading(self): + if DEBUG: + import traceback + print ('Attempt to use Tab Browser in non GUI thread') + traceback.print_stack() + def clear(self): + if not is_gui_thread(): + return self.debug_threading() if self.model(): self.model().clear_state() def is_visible(self, idx): + if not is_gui_thread(): + return self.debug_threading() item = idx.data(Qt.UserRole).toPyObject() if getattr(item, 'type', None) == TagTreeItem.TAG: idx = idx.parent() @@ -544,6 +567,9 @@ class TagsView(QTreeView): # {{{ Rebuild the category tree, expand any categories that were expanded, reset the search states, and reselect the current node. ''' + if not is_gui_thread(): + return self.debug_threading() + if self.disable_recounting or not self.pane_is_visible: return self.refresh_signal_processed = True @@ -570,6 +596,8 @@ class TagsView(QTreeView): # {{{ def show_item_at_index(self, idx, box=False, position=QTreeView.PositionAtCenter): + if not is_gui_thread(): + return self.debug_threading() if idx.isValid() and idx.data(Qt.UserRole).toPyObject() is not self._model.root_item: self.setCurrentIndex(idx) self.scrollTo(idx, position) From cc0aee27f5e23e29c7ba43afed026fb7373abb77 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 12:53:40 -0600 Subject: [PATCH 07/18] Revert thread check from Tag Browser since it doesn't prevent the crash --- src/calibre/gui2/tag_browser/view.py | 30 +--------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index ed1a597827..c833f7fa43 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -16,10 +16,9 @@ from PyQt4.Qt import (QItemDelegate, Qt, QTreeView, pyqtSignal, QSize, QIcon, from calibre.gui2.tag_browser.model import (TagTreeItem, TAG_SEARCH_STATES, TagsModel) -from calibre.gui2 import config, gprefs, is_gui_thread +from calibre.gui2 import config, gprefs from calibre.utils.search_query_parser import saved_searches from calibre.utils.icu import sort_key -from calibre.constants import DEBUG class TagDelegate(QItemDelegate): # {{{ @@ -126,8 +125,6 @@ class TagsView(QTreeView): # {{{ self.recount() def get_state(self): - if not is_gui_thread(): - return self.debug_threading() state_map = {} expanded_categories = [] for row, category in enumerate(self._model.category_nodes): @@ -139,13 +136,9 @@ class TagsView(QTreeView): # {{{ return expanded_categories, state_map def reread_collapse_parameters(self): - if not is_gui_thread(): - return self.debug_threading() self._model.reread_collapse_parameters(self.get_state()[1]) def set_database(self, db, tag_match, sort_by): - if not is_gui_thread(): - return self.debug_threading() self._model.set_database(db) self.pane_is_visible = True # because TagsModel.set_database did a recount @@ -172,8 +165,6 @@ class TagsView(QTreeView): # {{{ self.expanded.connect(self.item_expanded) def database_changed(self, event, ids): - if not is_gui_thread(): - return self.debug_threading() if self.refresh_signal_processed: self.refresh_signal_processed = False self.refresh_required.emit() @@ -200,8 +191,6 @@ class TagsView(QTreeView): # {{{ pass def set_search_restriction(self, s): - if not is_gui_thread(): - return self.debug_threading() s = s if s else None self._model.set_search_restriction(s) @@ -228,8 +217,6 @@ class TagsView(QTreeView): # {{{ set_to: if None, advance the state. Otherwise must be one of the values in TAG_SEARCH_STATES ''' - if not is_gui_thread(): - return self.debug_threading() modifiers = int(QApplication.keyboardModifiers()) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) if self._model.toggle(index, exclusive, set_to=set_to): @@ -542,21 +529,11 @@ class TagsView(QTreeView): # {{{ fm_src['display'].get('make_category', False)))): self.setDropIndicatorShown(True) - def debug_threading(self): - if DEBUG: - import traceback - print ('Attempt to use Tab Browser in non GUI thread') - traceback.print_stack() - def clear(self): - if not is_gui_thread(): - return self.debug_threading() if self.model(): self.model().clear_state() def is_visible(self, idx): - if not is_gui_thread(): - return self.debug_threading() item = idx.data(Qt.UserRole).toPyObject() if getattr(item, 'type', None) == TagTreeItem.TAG: idx = idx.parent() @@ -567,9 +544,6 @@ class TagsView(QTreeView): # {{{ Rebuild the category tree, expand any categories that were expanded, reset the search states, and reselect the current node. ''' - if not is_gui_thread(): - return self.debug_threading() - if self.disable_recounting or not self.pane_is_visible: return self.refresh_signal_processed = True @@ -596,8 +570,6 @@ class TagsView(QTreeView): # {{{ def show_item_at_index(self, idx, box=False, position=QTreeView.PositionAtCenter): - if not is_gui_thread(): - return self.debug_threading() if idx.isValid() and idx.data(Qt.UserRole).toPyObject() is not self._model.root_item: self.setCurrentIndex(idx) self.scrollTo(idx, position) From 2395ef81120f19c3c82303d733d57c4829050a49 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 13:28:40 -0600 Subject: [PATCH 08/18] Fix another crash in the Tag Browser --- src/calibre/gui2/tag_browser/view.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index c833f7fa43..2660d8f969 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -571,6 +571,9 @@ class TagsView(QTreeView): # {{{ def show_item_at_index(self, idx, box=False, position=QTreeView.PositionAtCenter): if idx.isValid() and idx.data(Qt.UserRole).toPyObject() is not self._model.root_item: + self.setExpanded(idx, True) # Needed otherwise Qt segfaults if the + # node is buried in a collapsed, off + # screen hierarchy self.setCurrentIndex(idx) self.scrollTo(idx, position) self.setCurrentIndex(idx) From ccfff7f456b3d347725977f73c34f9ba8bd71618 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 13:33:22 -0600 Subject: [PATCH 09/18] ... --- src/calibre/gui2/tag_browser/view.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index 2660d8f969..853fc296b5 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -574,9 +574,7 @@ class TagsView(QTreeView): # {{{ self.setExpanded(idx, True) # Needed otherwise Qt segfaults if the # node is buried in a collapsed, off # screen hierarchy - self.setCurrentIndex(idx) self.scrollTo(idx, position) - self.setCurrentIndex(idx) if box: self._model.set_boxed(idx) From f87375c45ed1f0cda3bf9a0602a50119fff178e1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 14:39:54 -0600 Subject: [PATCH 10/18] TB: When showing item at index, expand parent, not item --- src/calibre/gui2/tag_browser/view.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index 853fc296b5..39fd19c130 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -571,9 +571,10 @@ class TagsView(QTreeView): # {{{ def show_item_at_index(self, idx, box=False, position=QTreeView.PositionAtCenter): if idx.isValid() and idx.data(Qt.UserRole).toPyObject() is not self._model.root_item: - self.setExpanded(idx, True) # Needed otherwise Qt segfaults if the - # node is buried in a collapsed, off - # screen hierarchy + self.expand(self._model.parent(idx)) # Needed otherwise Qt sometimes segfaults if the + # node is buried in a collapsed, off + # screen hierarchy + self.setCurrentIndex(idx) self.scrollTo(idx, position) if box: self._model.set_boxed(idx) From fe1695b742e51eff5cb8ea84a81917d3c643efa2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 14:59:32 -0600 Subject: [PATCH 11/18] Support for detecting and mounting reader devices on FreeBSD. Fixes #802708 (Patches for Devices on FreeBSD) --- src/calibre/devices/linux_mount_helper.c | 18 +++ src/calibre/devices/usbms/device.py | 160 ++++++++++++++++++++++- 2 files changed, 177 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/linux_mount_helper.c b/src/calibre/devices/linux_mount_helper.c index 2ced0f31fa..550510106e 100644 --- a/src/calibre/devices/linux_mount_helper.c +++ b/src/calibre/devices/linux_mount_helper.c @@ -64,14 +64,24 @@ int do_mount(const char *dev, const char *mp) { snprintf(options, 1000, "rw,noexec,nosuid,sync,nodev"); snprintf(uids, 100, "%d", getuid()); snprintf(gids, 100, "%d", getgid()); +#else +#ifdef __FreeBSD__ + snprintf(options, 1000, "rw,noexec,nosuid,sync,-u=%d,-g=%d",getuid(),getgid()); #else snprintf(options, 1000, "rw,noexec,nosuid,sync,nodev,quiet,shortname=mixed,uid=%d,gid=%d,umask=077,fmask=0177,dmask=0077,utf8,iocharset=iso8859-1", getuid(), getgid()); #endif +#endif + ensure_root(); + #ifdef __NetBSD__ execlp("mount_msdos", "mount_msdos", "-u", uids, "-g", gids, "-o", options, dev, mp, NULL); +#else +#ifdef __FreeBSD__ + execlp("mount", "mount", "-t", "msdosfs", "-o", options, dev, mp, NULL); #else execlp("mount", "mount", "-t", "auto", "-o", options, dev, mp, NULL); +#endif #endif errsv = errno; fprintf(stderr, "Failed to mount with error: %s\n", strerror(errsv)); @@ -91,8 +101,12 @@ int call_eject(const char *dev, const char *mp) { ensure_root(); #ifdef __NetBSD__ execlp("eject", "eject", dev, NULL); +#else +#ifdef __FreeBSD__ + execlp("umount", "umount", dev, NULL); #else execlp("eject", "eject", "-s", dev, NULL); +#endif #endif /* execlp failed */ errsv = errno; @@ -121,7 +135,11 @@ int call_umount(const char *dev, const char *mp) { if (pid == 0) { /* Child process */ ensure_root(); +#ifdef __FreeBSD__ + execlp("umount", "umount", mp, NULL); +#else execlp("umount", "umount", "-l", mp, NULL); +#endif /* execlp failed */ errsv = errno; fprintf(stderr, "Failed to umount with error: %s\n", strerror(errsv)); diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 442f3701c4..bdbf5f44cf 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -17,7 +17,7 @@ from itertools import repeat from calibre.devices.interface import DevicePlugin from calibre.devices.errors import DeviceError, FreeSpaceError from calibre.devices.usbms.deviceconfig import DeviceConfig -from calibre.constants import iswindows, islinux, isosx, plugins +from calibre.constants import iswindows, islinux, isosx, isfreebsd, plugins from calibre.utils.filenames import ascii_filename as sanitize, shorten_components_to if isosx: @@ -701,7 +701,152 @@ class Device(DeviceConfig, DevicePlugin): self._card_a_prefix = self._card_b_prefix self._card_b_prefix = None +# ------------------------------------------------------ +# +# open for FreeBSD +# find the device node or nodes that match the S/N we already have from the scanner +# and attempt to mount each one +# 1. get list of disk devices from sysctl +# 2. compare that list with the one from camcontrol +# 3. and see if it has a matching s/n +# 6. find any partitions/slices associated with each node +# 7. attempt to mount, using calibre-mount-helper, each one +# 8. when finished, we have a list of mount points and associated device nodes +# + def open_freebsd(self): + # this gives us access to the S/N, etc. of the reader that the scanner has found + # and the match routines for some of that data, like s/n, vendor ID, etc. + d=self.detected_device + + if not d.serial: + raise DeviceError("Device has no S/N. Can't continue") + return False + + devs={} + di=0 + ndevs=4 # number of possible devices per reader (main, carda, cardb, launcher) + + #get list of disk devices + p=subprocess.Popen(["sysctl", "kern.disks"], stdout=subprocess.PIPE) + kdsks=subprocess.Popen(["sed", "s/kern.disks: //"], stdin=p.stdout, stdout=subprocess.PIPE).communicate()[0] + p.stdout.close() + #print kdsks + for dvc in kdsks.split(): + # for each one that's also in the list of cam devices ... + p=subprocess.Popen(["camcontrol", "devlist"], stdout=subprocess.PIPE) + devmatch=subprocess.Popen(["grep", dvc], stdin=p.stdout, stdout=subprocess.PIPE).communicate()[0] + p.stdout.close() + if devmatch: + #print "Checking ", devmatch + # ... see if we can get a S/N from the actual device node + sn=subprocess.Popen(["camcontrol", "inquiry", dvc, "-S"], stdout=subprocess.PIPE).communicate()[0] + sn=sn[0:-1] # drop the trailing newline + #print "S/N = ", sn + if sn and d.match_serial(sn): + # we have a matching s/n, record this device node + #print "match found: ", dvc + devs[di]=dvc + di += 1 + + # sort the list of devices + for i in range(1,ndevs+1): + for j in reversed(range(1,i)): + if devs[j-1] > devs[j]: + x=devs[j-1] + devs[j-1]=devs[j] + devs[j]=x + #print devs + + # now we need to see if any of these have slices/partitions + mtd=0 + label="READER" # could use something more unique, like S/N or productID... + cmd = '/usr/local/bin/calibre-mount-helper' + cmd = [cmd, 'mount'] + for i in range(0,ndevs): + cmd2="ls /dev/"+devs[i]+"*" + p=subprocess.Popen(cmd2, shell=True, stdout=subprocess.PIPE) + devs[i]=subprocess.Popen(["cut", "-d", "/", "-f" "3"], stdin=p.stdout, stdout=subprocess.PIPE).communicate()[0] + p.stdout.close() + + # try all the nodes to see what we can mount + for dev in devs[i].split(): + mp='/media/'+label+'-'+dev + #print "trying ", dev, "on", mp + try: + p = subprocess.Popen(cmd + ["/dev/"+dev, mp]) + except OSError: + raise DeviceError(_('Could not find mount helper: %s.')%cmd[0]) + while p.poll() is None: + time.sleep(0.1) + + if p.returncode == 0: + #print " mounted", dev + if i == 0: + self._main_prefix = mp + self._main_dev = "/dev/"+dev + #print "main = ", self._main_dev, self._main_prefix + if i == 1: + self._card_a_prefix = mp + self._card_a_dev = "/dev/"+dev + #print "card a = ", self._card_a_dev, self._card_a_prefix + if i == 2: + self._card_b_prefix = mp + self._card_b_dev = "/dev/"+dev + #print "card b = ", self._card_b_dev, self._card_b_prefix + + mtd += 1 + break + + if mtd > 0: + return True + else : + return False +# +# ------------------------------------------------------ +# +# this one is pretty simple: +# just umount each of the previously +# mounted filesystems, using the mount helper +# + def eject_freebsd(self): + cmd = '/usr/local/bin/calibre-mount-helper' + cmd = [cmd, 'eject'] + + if self._main_prefix: + #print "umount main:", cmd, self._main_dev, self._main_prefix + try: + p = subprocess.Popen(cmd + [self._main_dev, self._main_prefix]) + except OSError: + raise DeviceError( + _('Could not find mount helper: %s.')%cmd[0]) + while p.poll() is None: + time.sleep(0.1) + + if self._card_a_prefix: + #print "umount card a:", cmd, self._card_a_dev, self._card_a_prefix + try: + p = subprocess.Popen(cmd + [self._card_a_dev, self._card_a_prefix]) + except OSError: + raise DeviceError( + _('Could not find mount helper: %s.')%cmd[0]) + while p.poll() is None: + time.sleep(0.1) + + if self._card_b_prefix: + #print "umount card b:", cmd, self._card_b_dev, self._card_b_prefix + try: + p = subprocess.Popen(cmd + [self._card_b_dev, self._card_b_prefix]) + except OSError: + raise DeviceError( + _('Could not find mount helper: %s.')%cmd[0]) + while p.poll() is None: + time.sleep(0.1) + + self._main_prefix = None + self._card_a_prefix = None + self._card_b_prefix = None +# ------------------------------------------------------ def open(self, library_uuid): time.sleep(5) @@ -712,6 +857,14 @@ class Device(DeviceConfig, DevicePlugin): except DeviceError: time.sleep(7) self.open_linux() + if isfreebsd: + self._main_dev = self._card_a_dev = self._card_b_dev = None + try: + self.open_freebsd() + except DeviceError: + subprocess.Popen(["camcontrol", "rescan", "all"]) + time.sleep(2) + self.open_freebsd() if iswindows: try: self.open_windows() @@ -800,6 +953,11 @@ class Device(DeviceConfig, DevicePlugin): self.eject_linux() except: pass + if isfreebsd: + try: + self.eject_freebsd() + except: + pass if iswindows: try: self.eject_windows() From 64426fb0ea16901296424ddce43d02986454f7e0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 15:34:52 -0600 Subject: [PATCH 12/18] Fix handling of filenames that have an even number of periods before the file extension. Fixes #801939 (If html name ends with 3 periods, calibre will crash on conversion) --- src/calibre/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 33e80982d1..cf4d09770c 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -106,10 +106,12 @@ def sanitize_file_name(name, substitute='_', as_unicode=False): name = name.encode(filesystem_encoding, 'ignore') one = _filename_sanitize.sub(substitute, name) one = re.sub(r'\s', ' ', one).strip() - one = re.sub(r'^\.+$', '_', one) + bname, ext = os.path.splitext(one) + one = re.sub(r'^\.+$', '_', bname) if as_unicode: one = one.decode(filesystem_encoding) one = one.replace('..', substitute) + one += ext # Windows doesn't like path components that end with a period if one and one[-1] in ('.', ' '): one = one[:-1]+'_' @@ -132,8 +134,10 @@ def sanitize_file_name_unicode(name, substitute='_'): name] one = u''.join(chars) one = re.sub(r'\s', ' ', one).strip() - one = re.sub(r'^\.+$', '_', one) + bname, ext = os.path.splitext(one) + one = re.sub(r'^\.+$', '_', bname) one = one.replace('..', substitute) + one += ext # Windows doesn't like path components that end with a period or space if one and one[-1] in ('.', ' '): one = one[:-1]+'_' From 7d5fd85c53e2ca4043f828436054796a12aed2a1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 15:45:00 -0600 Subject: [PATCH 13/18] Fix a regression in 0.8.7 that broke reading metadata from MOBI files in the Edit metadata dialog. Fixes #801981 (Private bug) --- src/calibre/ebooks/mobi/reader.py | 5 ++++- src/calibre/library/database2.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 46505de4bd..1173b84266 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -957,7 +957,10 @@ def get_metadata(stream): return get_metadata(stream) from calibre.utils.logging import Log log = Log() - mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')]) + try: + mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')]) + except: + mi = MetaInformation(_('Unknown'), [_('Unknown')]) mh = MetadataHeader(stream, log) if mh.title and mh.title != _('Unknown'): mi.title = mh.title diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 9229d44cac..c8fd660e1a 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1245,6 +1245,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ret = tempfile.SpooledTemporaryFile(max_size=SPOOL_SIZE) shutil.copyfileobj(f, ret) ret.seek(0) + ret.name = f.name else: ret = f.read() return ret From d7b68a12d32b4389c7c637254c20266d6e3b695d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Jun 2011 15:53:16 -0600 Subject: [PATCH 14/18] ... --- src/calibre/ebooks/compression/palmdoc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/compression/palmdoc.c b/src/calibre/ebooks/compression/palmdoc.c index 6b07bb9cd5..922b63fe1b 100644 --- a/src/calibre/ebooks/compression/palmdoc.c +++ b/src/calibre/ebooks/compression/palmdoc.c @@ -54,7 +54,7 @@ cpalmdoc_decompress(PyObject *self, PyObject *args) { // Map chars to bytes for (j = 0; j < input_len; j++) input[j] = (_input[j] < 0) ? _input[j]+256 : _input[j]; - output = (char *)PyMem_Malloc(sizeof(char)*(MAX(BUFFER, 5*input_len))); + output = (char *)PyMem_Malloc(sizeof(char)*(MAX(BUFFER, 8*input_len))); if (output == NULL) return PyErr_NoMemory(); while (i < input_len) { From 448196eff35a4336b1e93ac46c955bb8ca3023da Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 28 Jun 2011 10:11:54 -0600 Subject: [PATCH 15/18] Add proxy info to FAQ --- src/calibre/manual/faq.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 97551b403f..c67d44b7d5 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -558,11 +558,16 @@ Most readers do not support this. You should complain to the manufacturer about Another alternative is to create a catalog in ebook form containing a listing of all the books in your calibre library, with their metadata. Click the arrow next to the convert button to access the catalog creation tool. And before you ask, no you cannot have the catalog "link directly to" books on your reader. +How do I get |app| to use my HTTP proxy? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, |app| uses whatever proxy settings are set in your OS. Sometimes these are incorrect, for example, on windows if you don't use Internet Explorer then the proxy settings may not be up to date. You can tell |app| to use a particular proxy server by setting the http_proxy environment variable. The format of the variable is: http://username:password@servername you should ask your network admin to give you the correct value for this variable. Note that |app| only supports HTTP proxies not SOCKS proxies. You can see the current proxies used by |app| in Preferences->Miscellaneous. + I want some feature added to |app|. What can I do? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You have two choices: 1. Create a patch by hacking on |app| and send it to me for review and inclusion. See `Development `_. - 2. `Open a ticket `_ (you have to register and login first). Remember that |app| development is done by volunteers, so if you get no response to your feature request, it means no one feels like implementing it. + 2. `Open a bug requesting the feature `_ . Remember that |app| development is done by volunteers, so if you get no response to your feature request, it means no one feels like implementing it. Why doesn't |app| have an automatic update? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From c417377ba7135682aaf9bdd13d3140da7e6985f3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 28 Jun 2011 10:17:22 -0600 Subject: [PATCH 16/18] ... --- recipes/ming_pao.recipe | 18 +++++++++--------- recipes/ming_pao_toronto.recipe | 18 +++++++++--------- recipes/ming_pao_vancouver.recipe | 18 +++++++++--------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/recipes/ming_pao.recipe b/recipes/ming_pao.recipe index 3566fca667..947d85692f 100644 --- a/recipes/ming_pao.recipe +++ b/recipes/ming_pao.recipe @@ -179,17 +179,17 @@ class MPRecipe(BasicNewsRecipe): def get_dtlocal(self): dt_utc = datetime.datetime.utcnow() if __Region__ == 'Hong Kong': - # convert UTC to local hk time - at HKT 4.30am, all news are available - dt_local = dt_utc + datetime.timedelta(8.0/24) - datetime.timedelta(4.5/24) - # dt_local = dt_utc.astimezone(pytz.timezone('Asia/Hong_Kong')) - datetime.timedelta(4.5/24) + # convert UTC to local hk time - at HKT 5.30am, all news are available + dt_local = dt_utc + datetime.timedelta(8.0/24) - datetime.timedelta(5.5/24) + # dt_local = dt_utc.astimezone(pytz.timezone('Asia/Hong_Kong')) - datetime.timedelta(5.5/24) elif __Region__ == 'Vancouver': - # convert UTC to local Vancouver time - at PST time 4.30am, all news are available - dt_local = dt_utc + datetime.timedelta(-8.0/24) - datetime.timedelta(4.5/24) - #dt_local = dt_utc.astimezone(pytz.timezone('America/Vancouver')) - datetime.timedelta(4.5/24) + # convert UTC to local Vancouver time - at PST time 5.30am, all news are available + dt_local = dt_utc + datetime.timedelta(-8.0/24) - datetime.timedelta(5.5/24) + #dt_local = dt_utc.astimezone(pytz.timezone('America/Vancouver')) - datetime.timedelta(5.5/24) elif __Region__ == 'Toronto': - # convert UTC to local Toronto time - at EST time 4.30am, all news are available - dt_local = dt_utc + datetime.timedelta(-5.0/24) - datetime.timedelta(4.5/24) - #dt_local = dt_utc.astimezone(pytz.timezone('America/Toronto')) - datetime.timedelta(4.5/24) + # convert UTC to local Toronto time - at EST time 8.30am, all news are available + dt_local = dt_utc + datetime.timedelta(-5.0/24) - datetime.timedelta(8.5/24) + #dt_local = dt_utc.astimezone(pytz.timezone('America/Toronto')) - datetime.timedelta(8.5/24) return dt_local def get_fetchdate(self): diff --git a/recipes/ming_pao_toronto.recipe b/recipes/ming_pao_toronto.recipe index 677a8272b0..9f3d7f510c 100644 --- a/recipes/ming_pao_toronto.recipe +++ b/recipes/ming_pao_toronto.recipe @@ -179,17 +179,17 @@ class MPRecipe(BasicNewsRecipe): def get_dtlocal(self): dt_utc = datetime.datetime.utcnow() if __Region__ == 'Hong Kong': - # convert UTC to local hk time - at HKT 4.30am, all news are available - dt_local = dt_utc + datetime.timedelta(8.0/24) - datetime.timedelta(4.5/24) - # dt_local = dt_utc.astimezone(pytz.timezone('Asia/Hong_Kong')) - datetime.timedelta(4.5/24) + # convert UTC to local hk time - at HKT 5.30am, all news are available + dt_local = dt_utc + datetime.timedelta(8.0/24) - datetime.timedelta(5.5/24) + # dt_local = dt_utc.astimezone(pytz.timezone('Asia/Hong_Kong')) - datetime.timedelta(5.5/24) elif __Region__ == 'Vancouver': - # convert UTC to local Vancouver time - at PST time 4.30am, all news are available - dt_local = dt_utc + datetime.timedelta(-8.0/24) - datetime.timedelta(4.5/24) - #dt_local = dt_utc.astimezone(pytz.timezone('America/Vancouver')) - datetime.timedelta(4.5/24) + # convert UTC to local Vancouver time - at PST time 5.30am, all news are available + dt_local = dt_utc + datetime.timedelta(-8.0/24) - datetime.timedelta(5.5/24) + #dt_local = dt_utc.astimezone(pytz.timezone('America/Vancouver')) - datetime.timedelta(5.5/24) elif __Region__ == 'Toronto': - # convert UTC to local Toronto time - at EST time 4.30am, all news are available - dt_local = dt_utc + datetime.timedelta(-5.0/24) - datetime.timedelta(4.5/24) - #dt_local = dt_utc.astimezone(pytz.timezone('America/Toronto')) - datetime.timedelta(4.5/24) + # convert UTC to local Toronto time - at EST time 8.30am, all news are available + dt_local = dt_utc + datetime.timedelta(-5.0/24) - datetime.timedelta(8.5/24) + #dt_local = dt_utc.astimezone(pytz.timezone('America/Toronto')) - datetime.timedelta(8.5/24) return dt_local def get_fetchdate(self): diff --git a/recipes/ming_pao_vancouver.recipe b/recipes/ming_pao_vancouver.recipe index 3312c8f7b8..3b13211d01 100644 --- a/recipes/ming_pao_vancouver.recipe +++ b/recipes/ming_pao_vancouver.recipe @@ -179,17 +179,17 @@ class MPRecipe(BasicNewsRecipe): def get_dtlocal(self): dt_utc = datetime.datetime.utcnow() if __Region__ == 'Hong Kong': - # convert UTC to local hk time - at HKT 4.30am, all news are available - dt_local = dt_utc + datetime.timedelta(8.0/24) - datetime.timedelta(4.5/24) - # dt_local = dt_utc.astimezone(pytz.timezone('Asia/Hong_Kong')) - datetime.timedelta(4.5/24) + # convert UTC to local hk time - at HKT 5.30am, all news are available + dt_local = dt_utc + datetime.timedelta(8.0/24) - datetime.timedelta(5.5/24) + # dt_local = dt_utc.astimezone(pytz.timezone('Asia/Hong_Kong')) - datetime.timedelta(5.5/24) elif __Region__ == 'Vancouver': - # convert UTC to local Vancouver time - at PST time 4.30am, all news are available - dt_local = dt_utc + datetime.timedelta(-8.0/24) - datetime.timedelta(4.5/24) - #dt_local = dt_utc.astimezone(pytz.timezone('America/Vancouver')) - datetime.timedelta(4.5/24) + # convert UTC to local Vancouver time - at PST time 5.30am, all news are available + dt_local = dt_utc + datetime.timedelta(-8.0/24) - datetime.timedelta(5.5/24) + #dt_local = dt_utc.astimezone(pytz.timezone('America/Vancouver')) - datetime.timedelta(5.5/24) elif __Region__ == 'Toronto': - # convert UTC to local Toronto time - at EST time 4.30am, all news are available - dt_local = dt_utc + datetime.timedelta(-5.0/24) - datetime.timedelta(4.5/24) - #dt_local = dt_utc.astimezone(pytz.timezone('America/Toronto')) - datetime.timedelta(4.5/24) + # convert UTC to local Toronto time - at EST time 8.30am, all news are available + dt_local = dt_utc + datetime.timedelta(-5.0/24) - datetime.timedelta(8.5/24) + #dt_local = dt_utc.astimezone(pytz.timezone('America/Toronto')) - datetime.timedelta(8.5/24) return dt_local def get_fetchdate(self): From fc02b538f5558a54aeb7a608ecbd2b17772265e1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 28 Jun 2011 11:05:44 -0600 Subject: [PATCH 17/18] ... --- src/calibre/library/database2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index c8fd660e1a..ef709cd85e 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1245,6 +1245,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ret = tempfile.SpooledTemporaryFile(max_size=SPOOL_SIZE) shutil.copyfileobj(f, ret) ret.seek(0) + # Various bits of code try to use the name as the default + # title when reading metadata, so set it ret.name = f.name else: ret = f.read() From d3a93c500b50f7a36aec753de4ea4e324b122b7e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 28 Jun 2011 12:06:24 -0600 Subject: [PATCH 18/18] Fix #802288 (ISBN ID not recognized when not lower case) --- src/calibre/gui2/metadata/basic_widgets.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 2d6c79d0e3..227a2257bc 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -1092,11 +1092,12 @@ class IdentifiersEdit(QLineEdit): # {{{ for x in parts: c = x.split(':') if len(c) > 1: - if c[0] == 'isbn': + itype = c[0].lower() + if itype == 'isbn': v = check_isbn(c[1]) if v is not None: c[1] = v - ans[c[0]] = c[1] + ans[itype] = c[1] return ans def fset(self, val): if not val: @@ -1112,7 +1113,7 @@ class IdentifiersEdit(QLineEdit): # {{{ if v is not None: val[k] = v ids = sorted(val.iteritems(), key=keygen) - txt = ', '.join(['%s:%s'%(k, v) for k, v in ids]) + txt = ', '.join(['%s:%s'%(k.lower(), v) for k, v in ids]) self.setText(txt.strip()) self.setCursorPosition(0) return property(fget=fget, fset=fset)