From 351ff91d6a5441c3e069a569ae1691af3f0d349d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 9 Jun 2010 12:40:03 +0100 Subject: [PATCH 01/13] Reset ondevice when books are deleted from the library or the device --- src/calibre/gui2/library/models.py | 11 ++++++++++- src/calibre/gui2/ui.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index abd80aaa8f..99516ce677 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -199,7 +199,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.count_changed() self.clear_caches() self.reset() - + return ids def delete_books_by_id(self, ids): for id in ids: @@ -881,6 +881,15 @@ class DeviceBooksModel(BooksModel): # {{{ ans.extend(v) return ans + def clear_ondevice(self, db_ids): + for data in self.db: + if data is None: + continue + app_id = getattr(data, 'application_id', None) + if app_id is not None and app_id in db_ids: + data.in_library = False + self.reset() + def flags(self, index): if self.map[index.row()] in self.indices_to_be_deleted(): return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 7d258608d0..cfbcb794f3 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -1546,7 +1546,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): row = None if ci.isValid(): row = ci.row() - view.model().delete_books(rows) + ids_deleted = view.model().delete_books(rows) + for v in (self.memory_view, self.card_a_view, self.card_b_view): + if v is None: + continue + v.model().clear_ondevice(ids_deleted) if row is not None: ci = view.model().index(row, 0) if ci.isValid(): @@ -1591,6 +1595,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.booklists()) model.paths_deleted(paths) self.upload_booklists() + # Clear the ondevice info so it will be recomputed + self.book_on_device(None, None, reset=True) + # We want to reset all the ondevice flags in the library. Use a big + # hammer, so we don't need to worry about whether some succeeded or not + self.library_view.model().refresh() ############################################################################ From bce229083b0cdcb42b20d9b8c7f4f7be323bb58c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 9 Jun 2010 17:42:56 +0100 Subject: [PATCH 02/13] 1) Force usbms to read metadata when building the cache_file 2) Generate a regexp from the save template --- src/calibre/devices/usbms/driver.py | 32 ++++++++++++++++++----------- src/calibre/ebooks/metadata/meta.py | 27 ++++++++++++++---------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 92e57e7447..1666398fc9 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -294,6 +294,19 @@ class USBMS(CLI, Device): self.report_progress(1.0, _('Sending metadata to device...')) debug_print('USBMS: finished sync_booklists') + @classmethod + def build_template_regexp(cls): + def replfunc(match): + if match.group(1) in ['title', 'series', 'series_index', 'isbn']: + return '(?P<' + match.group(1) + '>.+?)' + elif match.group(1) == 'authors': + return '(?P.+?)' + else: + return '(.+?)' + template = cls.save_template().rpartition('/')[2] + print 'bftr', template + return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)') + @classmethod def path_to_unicode(cls, path): if isbytestring(path): @@ -355,22 +368,17 @@ class USBMS(CLI, Device): from calibre.ebooks.metadata.meta import metadata_from_formats from calibre.customize.ui import quick_metadata with quick_metadata: - return metadata_from_formats(fmts) + return metadata_from_formats(fmts, force_read_metadata=True, + pattern=cls.build_template_regexp()) @classmethod - def book_from_path(cls, prefix, path): + def book_from_path(cls, prefix, lpath): from calibre.ebooks.metadata import MetaInformation - - if cls.settings().read_metadata or cls.MUST_READ_METADATA: - mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, path))) - else: - from calibre.ebooks.metadata.meta import metadata_from_filename - mi = metadata_from_filename(cls.normalize_path(os.path.basename(path)), - re.compile(r'^(?P[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+')) + mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath))) if mi is None: - mi = MetaInformation(os.path.splitext(os.path.basename(path))[0], + mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0], [_('Unknown')]) - size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size - book = cls.book_class(prefix, path, other=mi, size=size) + size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size + book = cls.book_class(prefix, lpath, other=mi, size=size) return book diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py index f5a327a0d6..eae8171362 100644 --- a/src/calibre/ebooks/metadata/meta.py +++ b/src/calibre/ebooks/metadata/meta.py @@ -27,16 +27,16 @@ for i, ext in enumerate(_METADATA_PRIORITIES): def path_to_ext(path): return os.path.splitext(path)[1][1:].lower() -def metadata_from_formats(formats): +def metadata_from_formats(formats, force_read_metadata=False, pattern=None): try: - return _metadata_from_formats(formats) + return _metadata_from_formats(formats, force_read_metadata, pattern) except: - mi = metadata_from_filename(list(iter(formats))[0]) + mi = metadata_from_filename(list(iter(formats), pattern)[0]) if not mi.authors: mi.authors = [_('Unknown')] return mi -def _metadata_from_formats(formats): +def _metadata_from_formats(formats, force_read_metadata=False, pattern=None): mi = MetaInformation(None, None) formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)], METADATA_PRIORITIES[path_to_ext(y)])) @@ -51,7 +51,9 @@ def _metadata_from_formats(formats): with open(path, 'rb') as stream: try: newmi = get_metadata(stream, stream_type=ext, - use_libprs_metadata=True) + use_libprs_metadata=True, + force_read_metadata=force_read_metadata, + pattern=pattern) mi.smart_update(newmi) except: continue @@ -69,18 +71,21 @@ def is_recipe(filename): return filename.startswith('calibre') and \ filename.rpartition('.')[0].endswith('_recipe_out') -def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False): +def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False, + force_read_metadata=False, pattern=None): pos = 0 if hasattr(stream, 'tell'): pos = stream.tell() try: - return _get_metadata(stream, stream_type, use_libprs_metadata) + return _get_metadata(stream, stream_type, use_libprs_metadata, + force_read_metadata, pattern) finally: if hasattr(stream, 'seek'): stream.seek(pos) -def _get_metadata(stream, stream_type, use_libprs_metadata): +def _get_metadata(stream, stream_type, use_libprs_metadata, + force_read_metadata=False, pattern=None): if stream_type: stream_type = stream_type.lower() if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'): stream_type = 'html' @@ -100,8 +105,8 @@ def _get_metadata(stream, stream_type, use_libprs_metadata): mi = MetaInformation(None, None) name = os.path.basename(getattr(stream, 'name', '')) - base = metadata_from_filename(name) - if is_recipe(name) or prefs['read_file_metadata']: + base = metadata_from_filename(name, pat=pattern) + if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']: mi = get_file_type_metadata(stream, stream_type) if base.title == os.path.splitext(name)[0] and base.authors is None: # Assume that there was no metadata in the file and the user set pattern @@ -139,7 +144,7 @@ def metadata_from_filename(name, pat=None): pat = re.compile(prefs.get('filename_pattern')) name = name.replace('_', ' ') match = pat.search(name) - if match: + if match is not None: try: mi.title = match.group('title') except IndexError: From eb73e3a33bbc35aa1821e444218d61f1ac90ded4 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 10:21:37 +0100 Subject: [PATCH 03/13] Put the flag back in usbms.driver to force metadata from paths, if that flag is set in the driver. Continue to ignore the flag set in the add-books dialog --- src/calibre/devices/usbms/driver.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 1666398fc9..0cd6a8f79c 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -374,8 +374,13 @@ class USBMS(CLI, Device): @classmethod def book_from_path(cls, prefix, lpath): from calibre.ebooks.metadata import MetaInformation - mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath))) + if cls.settings().read_metadata or cls.MUST_READ_METADATA: + mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath))) + else: + from calibre.ebooks.metadata.meta import metadata_from_filename + mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)), + cls.build_template_regexp()) if mi is None: mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0], [_('Unknown')]) From 03d4ee7b3f7133f5ce264f75ceac2136b7da523b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 11:38:35 +0100 Subject: [PATCH 04/13] search mixins --- src/calibre/gui2/init.py | 5 +- src/calibre/gui2/search_box.py | 77 ++++++++++- src/calibre/gui2/search_restriction_mixin.py | 56 ++++++++ src/calibre/gui2/ui.py | 135 +++---------------- 4 files changed, 150 insertions(+), 123 deletions(-) create mode 100644 src/calibre/gui2/search_restriction_mixin.py diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index a991c4d1f8..ed1035e2b6 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -275,7 +275,10 @@ class LibraryViewMixin(object): # {{{ if search: self.search.set_search_string(join.join(search)) - + def search_done(self, view, ok): + if view is self.current_view(): + self.search.search_done(ok) + self.set_number_of_books_shown() # }}} diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 4e9ccc2900..17a815c4ce 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -7,11 +7,15 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __docformat__ = 'restructuredtext en' from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \ - pyqtSignal, SIGNAL + pyqtSignal, SIGNAL, QObject, QDialog from PyQt4.QtGui import QCompleter from calibre.gui2 import config from calibre.gui2.dialogs.confirm_delete import confirm +from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor +from calibre.gui2.dialogs.search import SearchDialog +from calibre.utils.config import prefs +from calibre.utils.search_query_parser import saved_searches class SearchLineEdit(QLineEdit): @@ -79,8 +83,7 @@ class SearchBox2(QComboBox): self.setMinimumContentsLength(25) self._in_a_search = False - def initialize(self, opt_name, colorize=False, - help_text=_('Search')): + def initialize(self, opt_name, colorize=False, help_text=_('Search')): self.as_you_type = config['search_as_you_type'] self.opt_name = opt_name self.addItems(QStringList(list(set(config[opt_name])))) @@ -239,9 +242,9 @@ class SavedSearchBox(QComboBox): self.setInsertPolicy(self.NoInsert) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setMinimumContentsLength(10) + self.tool_tip_text = self.toolTip() def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')): - self.tool_tip_text = self.toolTip() self.saved_searches = _saved_searches self.search_box = _search_box self.help_text = help_text @@ -331,3 +334,69 @@ class SavedSearchBox(QComboBox): if idx < 0: return self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText()))) + +class SearchBoxMixin(object): + + def __init__(self): + self.search.initialize('main_search_history', colorize=True, + help_text=_('Search (For Advanced Search click the button to the left)')) + self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared) + self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear) + QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'), + self.do_advanced_search) + + self.search.clear() + self.search.setFocus(Qt.OtherFocusReason) + self.search.setMaximumWidth(self.width()-150) + + def search_box_cleared(self): + self.tags_view.clear() + self.saved_search.clear_to_help() + self.set_number_of_books_shown() + + def do_advanced_search(self, *args): + d = SearchDialog(self) + if d.exec_() == QDialog.Accepted: + self.search.set_search_string(d.search_string()) + +class SavedSearchBoxMixin(object): + + def __init__(self): + self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed) + self.saved_searches_changed() + self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help) + self.saved_search.initialize(saved_searches, self.search, colorize=True, + help_text=_('Saved Searches')) + self.connect(self.save_search_button, SIGNAL('clicked()'), + self.saved_search.save_search_button_clicked) + self.connect(self.delete_search_button, SIGNAL('clicked()'), + self.saved_search.delete_search_button_clicked) + self.connect(self.copy_search_button, SIGNAL('clicked()'), + self.saved_search.copy_search_button_clicked) + + + def saved_searches_changed(self): + p = prefs['saved_searches'].keys() + p.sort() + t = unicode(self.search_restriction.currentText()) + self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches + self.search_restriction.addItem('') + self.tags_view.recount() + for s in p: + self.search_restriction.addItem(s) + if t: + if t in p: # redo the current restriction, if there was one + self.search_restriction.setCurrentIndex(self.search_restriction.findText(t)) + # self.tags_view.set_search_restriction(t) + else: + self.search_restriction.setCurrentIndex(0) + self.apply_search_restriction('') + + def do_saved_search_edit(self, search): + d = SavedSearchEditor(self, search) + d.exec_() + if d.result() == d.Accepted: + self.saved_searches_changed() + self.saved_search.clear_to_help() + + diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py new file mode 100644 index 0000000000..b25e202352 --- /dev/null +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -0,0 +1,56 @@ +''' +Created on 10 Jun 2010 + +@author: charles +''' + +class SearchRestrictionMixin(object): + + def __init__(self): + self.library_view.model().count_changed_signal.connect(self.restriction_count_changed) + self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon) + self.search_restriction.setMinimumContentsLength(10) + + ''' + Adding and deleting books while restricted creates a complexity. When added, + they are displayed regardless of whether they match a search restriction. + However, if they do not, they are removed at the next search. The counts + must take this behavior into effect. + ''' + + def restriction_count_changed(self, c): + self.restriction_count_of_books_in_view += \ + c - self.restriction_count_of_books_in_library + self.restriction_count_of_books_in_library = c + if self.restriction_in_effect: + self.set_number_of_books_shown() + + def apply_search_restriction(self, r): + r = unicode(r) + if r is not None and r != '': + self.restriction_in_effect = True + restriction = 'search:"%s"'%(r) + else: + self.restriction_in_effect = False + restriction = '' + self.restriction_count_of_books_in_view = \ + self.library_view.model().set_search_restriction(restriction) + self.search.clear_to_help() + self.saved_search.clear_to_help() + self.tags_view.set_search_restriction(restriction) + self.set_number_of_books_shown() + + def set_number_of_books_shown(self): + if self.current_view() == self.library_view and self.restriction_in_effect: + t = _("({0} of {1})").format(self.current_view().row_count(), + self.restriction_count_of_books_in_view) + self.search_count.setStyleSheet \ + ('QLabel { border-radius: 8px; background-color: yellow; }') + else: # No restriction or not library view + if not self.search.in_a_search(): + t = _("(all books)") + else: + t = _("({0} of all)").format(self.current_view().row_count()) + self.search_count.setStyleSheet( + 'QLabel { background-color: transparent; }') + self.search_count.setText(t) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index b342728ff4..675261d09d 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -29,7 +29,6 @@ from calibre.utils.filenames import ascii_filename from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import prefs, dynamic from calibre.utils.ipc.server import Server -from calibre.utils.search_query_parser import saved_searches from calibre.devices.errors import UserFeedback from calibre.gui2 import warning_dialog, choose_files, error_dialog, \ question_dialog,\ @@ -51,7 +50,6 @@ from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \ fetch_scheduled_recipe, generate_catalog from calibre.gui2.dialogs.config import ConfigDialog -from calibre.gui2.dialogs.search import SearchDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.book_info import BookInfo from calibre.ebooks import BOOK_EXTENSIONS @@ -59,9 +57,10 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString from calibre.library.database2 import LibraryDatabase2 from calibre.library.caches import CoverCache from calibre.gui2.dialogs.confirm_delete import confirm -from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor -from calibre.gui2.tag_view import TagBrowserMixin from calibre.gui2.init import ToolbarMixin, LibraryViewMixin +from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin +from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin +from calibre.gui2.tag_view import TagBrowserMixin class Listener(Thread): # {{{ @@ -106,7 +105,8 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{ # }}} class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, - TagBrowserMixin, CoverFlowMixin, LibraryViewMixin): + TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, + SavedSearchBoxMixin, SearchRestrictionMixin): 'The main GUI' def set_default_thumbnail(self, height): @@ -149,20 +149,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.restriction_count_of_books_in_view = 0 self.restriction_count_of_books_in_library = 0 self.restriction_in_effect = False - self.search.initialize('main_search_history', colorize=True, - help_text=_('Search (For Advanced Search click the button to the left)')) - self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear) - self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help) - self.search.clear() - - self.saved_search.initialize(saved_searches, self.search, colorize=True, - help_text=_('Saved Searches')) - self.connect(self.save_search_button, SIGNAL('clicked()'), - self.saved_search.save_search_button_clicked) - self.connect(self.delete_search_button, SIGNAL('clicked()'), - self.saved_search.delete_search_button_clicked) - self.connect(self.copy_search_button, SIGNAL('clicked()'), - self.saved_search.copy_search_button_clicked) self.progress_indicator = ProgressIndicator(self) self.verbose = opts.verbose @@ -225,8 +211,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.connect(self.system_tray_icon, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), self.system_tray_icon_activated) - QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'), - self.do_advanced_search) DeviceMixin.__init__(self) @@ -265,6 +249,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.update_checker.update_found.connect(self.update_found, type=Qt.QueuedConnection) self.update_checker.start() + ####################### Status Bar ##################### self.status_bar.initialize(self.system_tray_icon) self.status_bar.show_book_info.connect(self.show_book_info) @@ -273,6 +258,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, ####################### Setup Toolbar ##################### ToolbarMixin.__init__(self) + ####################### Search boxes ######################## + SavedSearchBoxMixin.__init__(self) + SearchBoxMixin.__init__(self) + ####################### Library view ######################## LibraryViewMixin.__init__(self, db) @@ -281,19 +270,13 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, if self.system_tray_icon.isVisible() and opts.start_in_tray: self.hide_windows() self.stack.setCurrentIndex(0) - self.search.setFocus(Qt.OtherFocusReason) self.cover_cache = CoverCache(self.library_path) self.cover_cache.start() self.library_view.model().cover_cache = self.cover_cache self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit) self.search_restriction.activated[str].connect(self.apply_search_restriction) - for x in (self.location_view.count_changed, self.tags_view.recount, - self.restriction_count_changed): - self.library_view.model().count_changed_signal.connect(x) - - self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared) - self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed) - self.saved_searches_changed() + self.library_view.model().count_changed_signal.connect \ + (self.location_view.count_changed) if not gprefs.get('quick_start_guide_added', False): from calibre.ebooks.metadata import MetaInformation mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember']) @@ -313,9 +296,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, type=Qt.QueuedConnection) ########################### Tags Browser ############################## + + self.library_view.model().count_changed_signal.connect(self.tags_view.recount) TagBrowserMixin.__init__(self, db) - self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon) - self.search_restriction.setMinimumContentsLength(10) + + ######################### Search Restriction ########################## + SearchRestrictionMixin.__init__(self) ########################### Cover Flow ################################ @@ -324,7 +310,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self._calculated_available_height = min(max_available_height()-15, self.height()) self.resize(self.width(), self._calculated_available_height) - self.search.setMaximumWidth(self.width()-150) # Jobs Button {{{ self.jobs_button = JobsButton() @@ -363,13 +348,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) - def do_saved_search_edit(self, search): - d = SavedSearchEditor(self, search) - d.exec_() - if d.result() == d.Accepted: - self.saved_searches_changed() - self.saved_search.clear_to_help() - def resizeEvent(self, ev): MainWindow.resizeEvent(self, ev) self.search.setMaximumWidth(self.width()-150) @@ -451,76 +429,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, error_dialog(self, _('Failed to start content server'), unicode(self.content_server.exception)).exec_() - ''' - Restrictions. - Adding and deleting books creates a complexity. When added, they are - displayed regardless of whether they match a search restriction. However, if - they do not, they are removed at the next search. The counts must take this - behavior into effect. - ''' - - def restriction_count_changed(self, c): - self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library - self.restriction_count_of_books_in_library = c - if self.restriction_in_effect: - self.set_number_of_books_shown() - - def apply_search_restriction(self, r): - r = unicode(r) - if r is not None and r != '': - self.restriction_in_effect = True - restriction = 'search:"%s"'%(r) - else: - self.restriction_in_effect = False - restriction = '' - self.restriction_count_of_books_in_view = \ - self.library_view.model().set_search_restriction(restriction) - self.search.clear_to_help() - self.saved_search.clear_to_help() - self.tags_view.set_search_restriction(restriction) - self.set_number_of_books_shown() - - def set_number_of_books_shown(self): - if self.current_view() == self.library_view and self.restriction_in_effect: - t = _("({0} of {1})").format(self.current_view().row_count(), - self.restriction_count_of_books_in_view) - self.search_count.setStyleSheet('QLabel { border-radius: 8px; background-color: yellow; }') - else: # No restriction or not library view - if not self.search.in_a_search(): - t = _("(all books)") - else: - t = _("({0} of all)").format(self.current_view().row_count()) - self.search_count.setStyleSheet( - 'QLabel { background-color: transparent; }') - self.search_count.setText(t) - - def search_box_cleared(self): - self.tags_view.clear() - self.saved_search.clear_to_help() - self.set_number_of_books_shown() - - def search_done(self, view, ok): - if view is self.current_view(): - self.search.search_done(ok) - self.set_number_of_books_shown() - - def saved_searches_changed(self): - p = prefs['saved_searches'].keys() - p.sort() - t = unicode(self.search_restriction.currentText()) - self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches - self.search_restriction.addItem('') - self.tags_view.recount() - for s in p: - self.search_restriction.addItem(s) - if t: - if t in p: # redo the current restriction, if there was one - self.search_restriction.setCurrentIndex(self.search_restriction.findText(t)) - # self.tags_view.set_search_restriction(t) - else: - self.search_restriction.setCurrentIndex(0) - self.apply_search_restriction('') - def another_instance_wants_to_talk(self): try: @@ -559,8 +467,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, def booklists(self): return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db - - ########################## Connect to device ############################## def save_device_view_settings(self): @@ -1859,13 +1765,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, ############################################################################ - ########################### Do advanced search ############################# - - def do_advanced_search(self, *args): - d = SearchDialog(self) - if d.exec_() == QDialog.Accepted: - self.search.set_search_string(d.search_string()) - ############################################################################ ############################### Do config ################################## From a88d51134cf3e73c97ceda326faddc28662a90c7 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 13:00:12 +0100 Subject: [PATCH 05/13] Bug #5774: make ondevice match both author and author_sort --- src/calibre/gui2/device.py | 40 ++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 1445b0f36e..6926c751b2 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1140,6 +1140,13 @@ class DeviceMixin(object): in cache['authors']: loc[i] = True continue + # Also check author sort, because it can be used as author in + # some formats + if mi.author_sort and \ + re.sub('(?u)\W|[_]', '', mi.author_sort.lower()) \ + in cache['authors']: + loc[i] = True + continue return loc def set_books_in_library(self, booklists, reset=False): @@ -1152,10 +1159,16 @@ class DeviceMixin(object): mi = db.get_metadata(id, index_is_id=True) title = re.sub('(?u)\W|[_]', '', mi.title.lower()) if title not in self.db_book_title_cache: - self.db_book_title_cache[title] = {'authors':{}, 'db_ids':{}} - authors = authors_to_string(mi.authors).lower() if mi.authors else '' - authors = re.sub('(?u)\W|[_]', '', authors) - self.db_book_title_cache[title]['authors'][authors] = mi + self.db_book_title_cache[title] = \ + {'authors':{}, 'author_sort':{}, 'db_ids':{}} + if mi.authors: + authors = authors_to_string(mi.authors).lower() + authors = re.sub('(?u)\W|[_]', '', authors) + self.db_book_title_cache[title]['authors'][authors] = mi + if mi.author_sort: + aus = mi.author_sort.lower() + aus = re.sub('(?u)\W|[_]', '', aus) + self.db_book_title_cache[title]['author_sort'][aus] = mi self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi self.db_book_uuid_cache.add(mi.uuid) @@ -1186,12 +1199,19 @@ class DeviceMixin(object): book.smart_update(d['db_ids'][book.db_id]) resend_metadata = True continue - book_authors = authors_to_string(book.authors).lower() if book.authors else '' - book_authors = re.sub('(?u)\W|[_]', '', book_authors) - if book_authors in d['authors']: - book.in_library = True - book.smart_update(d['authors'][book_authors]) - resend_metadata = True + if book.authors: + # Compare against both author and author sort, because + # either can appear as the author + book_authors = authors_to_string(book.authors).lower() + book_authors = re.sub('(?u)\W|[_]', '', book_authors) + if book_authors in d['authors']: + book.in_library = True + book.smart_update(d['authors'][book_authors]) + resend_metadata = True + elif book_authors in d['author_sort']: + book.in_library = True + book.smart_update(d['author_sort'][book_authors]) + resend_metadata = True # Set author_sort if it isn't already asort = getattr(book, 'author_sort', None) if not asort and book.authors: From e25b0e7f41de0917255afcc2e5411a30df1caf3d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 13:48:05 +0100 Subject: [PATCH 06/13] Remove a print statement --- src/calibre/devices/usbms/driver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 0cd6a8f79c..6f558b9b34 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -304,7 +304,6 @@ class USBMS(CLI, Device): else: return '(.+?)' template = cls.save_template().rpartition('/')[2] - print 'bftr', template return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)') @classmethod From d2677fc1bb1bc83251e42a1bce5f78f922add53e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 17:21:17 +0100 Subject: [PATCH 07/13] Fixup date on windows in sony driver --- src/calibre/devices/prs505/sony_cache.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/prs505/sony_cache.py b/src/calibre/devices/prs505/sony_cache.py index 5542e28d90..413d6959a6 100644 --- a/src/calibre/devices/prs505/sony_cache.py +++ b/src/calibre/devices/prs505/sony_cache.py @@ -12,7 +12,7 @@ from uuid import uuid4 from lxml import etree -from calibre import prints, guess_type +from calibre import prints, guess_type, iswindows from calibre.devices.errors import DeviceError from calibre.devices.usbms.driver import debug_print from calibre.constants import DEBUG @@ -423,7 +423,10 @@ class XMLCache(object): return ans def update_text_record(self, record, book, path, bl_index): - timestamp = os.path.getctime(path) + timestamp = os.path.getmtime(path) + # Correct for MS DST time 'adjustment' + if iswindows and time.daylight: + timestamp -= time.altzone - time.timezone date = strftime(timestamp) if date != record.get('date', None): record.set('date', date) From fe1316f201d541e2cbc6db34120b0daca6f66559 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 21:16:54 +0100 Subject: [PATCH 08/13] Title sort tweak. --- resources/default_tweaks.py | 10 ++++++++++ src/calibre/ebooks/metadata/__init__.py | 9 +++++---- src/calibre/gui2/cover_flow.py | 4 +++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index e9ad64cee2..9f58ab3f03 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -61,3 +61,13 @@ sort_columns_at_startup = None # default if not set: MMM yyyy gui_pubdate_display_format = 'MMM yyyy' +# Control title sorting. +# If set to 'library_order', Leading articles such as The and A will be ignored. +# If set to 'strictly_alphabetic', the titles will be sorted without processing +# For example, with library_order, The Client will sort under 'C'. With +# strictly_alphabetic, the book will sort under 'T'. +# This flag affects Calibre's library display. It has no effect on devices. In +# addition, books added before changing the flag will retain their order until +# the title is edited. Double-clicking on a title and hitting return without +# changing anything is sufficient to change the sort. +title_sorting = 'library_order' diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 6b573a0420..e6551e9019 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -43,10 +43,11 @@ def authors_to_sort_string(authors): _title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE) def title_sort(title): - match = _title_pat.search(title) - if match: - prep = match.group(1) - title = title[len(prep):] + ', ' + prep + if tweaks['title_sorting'] == 'library_order': + match = _title_pat.search(title) + if match: + prep = match.group(1) + title = title[len(prep):] + ', ' + prep return title.strip() coding = zip( diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index 3bd554e891..27f8687b22 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -67,7 +67,9 @@ if pictureflow is not None: return ans def reset(self): - self.dataChanged.emit() + from PyQt4.Qt import SIGNAL ### TEMP + self.emit(SIGNAL('dataChanged()')) # TEMP +# self.dataChanged.emit() def image(self, index): return self.model.cover(index) From 1411f34b15fded7fc770349595f76d0d6a65bf62 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 21:29:37 +0100 Subject: [PATCH 09/13] Change to reduce impact of the tweak to only the DB --- src/calibre/ebooks/metadata/__init__.py | 10 ++++------ src/calibre/library/database2.py | 15 +++++++++++---- src/calibre/library/sqlite.py | 6 +++++- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index e6551e9019..b4ba6683c0 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -12,7 +12,6 @@ from urlparse import urlparse from calibre import relpath, prints -from calibre.utils.config import tweaks from calibre.utils.date import isoformat _author_pat = re.compile(',?\s+(and|with)\s+', re.IGNORECASE) @@ -43,11 +42,10 @@ def authors_to_sort_string(authors): _title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE) def title_sort(title): - if tweaks['title_sorting'] == 'library_order': - match = _title_pat.search(title) - if match: - prep = match.group(1) - title = title[len(prep):] + ', ' + prep + match = _title_pat.search(title) + if match: + prep = match.group(1) + title = title[len(prep):] + ', ' + prep return title.strip() coding = zip( diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 5868a782ad..64fcfd7a6e 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -28,7 +28,7 @@ from calibre.customize.ui import run_plugins_on_import from calibre.utils.filenames import ascii_filename from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp -from calibre.utils.config import prefs +from calibre.utils.config import prefs, tweaks from calibre.utils.search_query_parser import saved_searches from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format from calibre.utils.magick_draw import save_cover_data_to @@ -736,8 +736,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): icon=icon, tooltip = tooltip) for r in data if item_not_zero_func(r)] if category == 'series' and not sort_on_count: - categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(), - title_sort(y.name).lower())) + if tweaks['title_sorting'] == 'library_order': + ts = lambda x: title_sort(x) + else: + ts = lambda x:x + categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(), + ts(y.name).lower())) # We delayed computing the standard formats category because it does not # use a view, but is computed dynamically @@ -950,7 +954,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): title = title.decode(preferred_encoding, 'replace') self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id)) self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True) - self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True) + if tweaks['title_sorting'] == 'library_order': + self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True) + else: + self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True) self.set_path(id, True) self.conn.commit() if notify: diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index d95eae9226..ca501b9300 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -15,6 +15,7 @@ from threading import RLock from datetime import datetime from calibre.ebooks.metadata import title_sort +from calibre.utils.config import tweaks from calibre.utils.date import parse_date, isoformat global_lock = RLock() @@ -115,7 +116,10 @@ class DBThread(Thread): self.conn.create_aggregate('concat', 1, Concatenate) self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate) - self.conn.create_function('title_sort', 1, title_sort) + if tweaks['title_sorting'] == 'library_order': + self.conn.create_function('title_sort', 1, title_sort) + else: + self.conn.create_function('title_sort', 1, lambda x:x) self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4())) # Dummy functions for dynamically created filters self.conn.create_function('books_list_filter', 1, lambda x: 1) From c1c1f43654939d467b370d6431fad4b388ff2270 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 21:31:02 +0100 Subject: [PATCH 10/13] Fix up deleted import that shouldn't have been deleted --- src/calibre/ebooks/metadata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index b4ba6683c0..55d70ee909 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -11,7 +11,7 @@ from urllib import unquote, quote from urlparse import urlparse from calibre import relpath, prints - +from calibre.utils.config import tweaks from calibre.utils.date import isoformat _author_pat = re.compile(',?\s+(and|with)\s+', re.IGNORECASE) From 93a425d76719e65f28a208503371882f35b9af33 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 21:32:05 +0100 Subject: [PATCH 11/13] Back out temp change to cover_flow --- src/calibre/gui2/cover_flow.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index 27f8687b22..3bd554e891 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -67,9 +67,7 @@ if pictureflow is not None: return ans def reset(self): - from PyQt4.Qt import SIGNAL ### TEMP - self.emit(SIGNAL('dataChanged()')) # TEMP -# self.dataChanged.emit() + self.dataChanged.emit() def image(self, index): return self.model.cover(index) From 30c209fd7e1c61d8df3107967424958fd1dba4ed Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 21:33:44 +0100 Subject: [PATCH 12/13] Make metadata.__init__ be identical to previous --- src/calibre/ebooks/metadata/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 55d70ee909..6b573a0420 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -11,6 +11,7 @@ from urllib import unquote, quote from urlparse import urlparse from calibre import relpath, prints + from calibre.utils.config import tweaks from calibre.utils.date import isoformat From cbe0b78aefc8c03f2a8da4886089d8fe42e5c946 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 10 Jun 2010 21:48:01 +0100 Subject: [PATCH 13/13] Make series sorting on library and tag views honor the the title_series_sorting flag (and rename the flag --- resources/default_tweaks.py | 10 +++++----- src/calibre/library/caches.py | 9 ++++++--- src/calibre/library/database2.py | 4 ++-- src/calibre/library/sqlite.py | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 9f58ab3f03..bda839b28f 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -61,13 +61,13 @@ sort_columns_at_startup = None # default if not set: MMM yyyy gui_pubdate_display_format = 'MMM yyyy' -# Control title sorting. +# Control title and series sorting in the library view. # If set to 'library_order', Leading articles such as The and A will be ignored. # If set to 'strictly_alphabetic', the titles will be sorted without processing # For example, with library_order, The Client will sort under 'C'. With # strictly_alphabetic, the book will sort under 'T'. # This flag affects Calibre's library display. It has no effect on devices. In -# addition, books added before changing the flag will retain their order until -# the title is edited. Double-clicking on a title and hitting return without -# changing anything is sufficient to change the sort. -title_sorting = 'library_order' +# addition, titles for books added before changing the flag will retain their +# order until the title is edited. Double-clicking on a title and hitting return +# without changing anything is sufficient to change the sort. +title_series_sorting = 'library_order' diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index e2ecdd9f55..95ca4cc826 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -619,9 +619,12 @@ class ResultCache(SearchQueryParser): if self.first_sort: subsort = True self.first_sort = False - fcmp = self.seriescmp if field == 'series' else \ - functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort, - asstr=as_string) + fcmp = self.seriescmp \ + if field == 'series' and \ + tweaks['title_sorting'] == 'library_order' \ + else \ + functools.partial(self.cmp, self.FIELD_MAP[field], + subsort=subsort, asstr=as_string) self._map.sort(cmp=fcmp, reverse=not ascending) self._map_filtered = [id for id in self._map if id in self._map_filtered] diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 64fcfd7a6e..0168737fca 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -736,7 +736,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): icon=icon, tooltip = tooltip) for r in data if item_not_zero_func(r)] if category == 'series' and not sort_on_count: - if tweaks['title_sorting'] == 'library_order': + if tweaks['title_series_sorting'] == 'library_order': ts = lambda x: title_sort(x) else: ts = lambda x:x @@ -954,7 +954,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): title = title.decode(preferred_encoding, 'replace') self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id)) self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True) - if tweaks['title_sorting'] == 'library_order': + if tweaks['title_series_sorting'] == 'library_order': self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True) else: self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True) diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index ca501b9300..adf6691671 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -116,7 +116,7 @@ class DBThread(Thread): self.conn.create_aggregate('concat', 1, Concatenate) self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate) - if tweaks['title_sorting'] == 'library_order': + if tweaks['title_series_sorting'] == 'library_order': self.conn.create_function('title_sort', 1, title_sort) else: self.conn.create_function('title_sort', 1, lambda x:x)