From 779922d1f33a483b912ce53de03438fca15cc835 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 24 Dec 2010 11:25:23 +0000 Subject: [PATCH 01/58] Enhancement #8030: View "next testexample" in Multiple Value Metadata Fields --- src/calibre/gui2/dialogs/metadata_bulk.py | 25 +++++++-- src/calibre/gui2/dialogs/metadata_bulk.ui | 68 +++++++++++++++++------ 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index dc691c4ffe..bde5cae128 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -414,6 +414,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.s_r_template.completer().setCaseSensitivity(Qt.CaseSensitive) self.s_r_search_mode_changed(self.search_mode.currentIndex()) + self.multiple_separator.setFixedWidth(30) + self.multiple_separator.setText(' ::: ') + self.multiple_separator.textChanged.connect(self.s_r_separator_changed) def s_r_get_field(self, mi, field): if field: @@ -451,19 +454,22 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): mi = self.db.get_metadata(self.ids[i], index_is_id=True) src = unicode(self.search_field.currentText()) t = self.s_r_get_field(mi, src) - w.setText(''.join(t[0:1])) + w.setText(unicode(self.multiple_separator.text()).join(t)) if self.search_mode.currentIndex() == 0: self.destination_field.setCurrentIndex(idx) else: + self.s_r_destination_field_changed(self.destination_field.currentText()) self.s_r_paint_results(None) def s_r_destination_field_changed(self, txt): txt = unicode(txt) + if not txt: + txt = unicode(self.search_field.currentText()) self.comma_separated.setEnabled(True) - if txt: - fm = self.db.metadata_for_field(txt) - if fm['is_multiple']: + if txt and txt in self.writable_fields: + self.destination_field_fm = self.db.metadata_for_field(txt) + if self.destination_field_fm['is_multiple']: self.comma_separated.setEnabled(False) self.comma_separated.setChecked(True) self.s_r_paint_results(None) @@ -493,6 +499,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.s_r_heading.setText('

'+self.main_heading + self.regexp_heading) self.s_r_paint_results(None) + def s_r_separator_changed(self, txt): + self.s_r_search_field_changed(self.search_field.currentIndex()) + def s_r_set_colors(self): if self.s_r_error is not None: col = 'rgb(255, 0, 0, 20%)' @@ -592,8 +601,12 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): wr = getattr(self, 'book_%d_result'%(i+1)) try: result = self.s_r_do_regexp(mi) - t = self.s_r_do_destination(mi, result[0:1]) - t = self.s_r_replace_mode_separator().join(t) + t = self.s_r_do_destination(mi, result) + if len(result) > 1 and self.destination_field_fm is not None and \ + self.destination_field_fm['is_multiple']: + t = unicode(self.multiple_separator.text()).join(t) + else: + t = self.s_r_replace_mode_separator().join(t) wr.setText(t) except Exception as e: self.s_r_error = e diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index dca7abc82c..d945909f96 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -478,7 +478,7 @@ Future conversion of these books will use the default settings. - Search mode: + Search &mode: search_mode @@ -559,7 +559,7 @@ Future conversion of these books will use the default settings. Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored - Case sensitive + Cas&e sensitive true @@ -588,7 +588,7 @@ Future conversion of these books will use the default settings. - Apply function after replace: + &Apply function after replace: replace_func @@ -641,7 +641,7 @@ If blank, the source field is used if the field is modifiable - Mode: + M&ode: replace_mode @@ -658,11 +658,11 @@ If blank, the source field is used if the field is modifiable - If the replace mode is prepend or append, then this box indicates whether a comma or -nothing should be put between the original text and the inserted text + Specifies whether a comma should be put between values when copying from a +multiple-valued field to a single-valued field - use comma + &Use comma true @@ -687,7 +687,7 @@ nothing should be put between the original text and the inserted text - Test &text + Test text test_text @@ -695,14 +695,48 @@ nothing should be put between the original text and the inserted text - - - Test re&sult - - - test_result - - + + + + + Test result + + + test_result + + + + + + + Qt::Horizontal + + + + 20 + 0 + + + + + + + + Multi&ple separator: + + + multiple_separator + + + + + + + Used when displaying test results to separate values in multiple-valued fields + + + + @@ -823,7 +857,7 @@ nothing should be put between the original text and the inserted text destination_field replace_mode comma_separated - scrollArea11 + multiple_separator test_text test_result From 68e32e6ee51456bea907413e98409c59b5b70f50 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 24 Dec 2010 13:11:02 +0000 Subject: [PATCH 02/58] Enhancement #8033: Sony collection name format changes --- resources/default_tweaks.py | 61 ++++++++++++++++++++---------- src/calibre/devices/usbms/books.py | 34 +++++++++++++---- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index a420cd7d44..d01999e766 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -135,32 +135,53 @@ auto_connect_to_folder = '' # metadata management is set to automatic. Collections on Sonys are named # depending upon whether the field is standard or custom. A collection derived # from a standard field is named for the value in that field. For example, if -# the standard 'series' column contains the name 'Darkover', then the series -# will be named 'Darkover'. A collection derived from a custom field will have -# the name of the field added to the value. For example, if a custom series +# the standard 'series' column contains the value 'Darkover', then the +# collection name is 'Darkover'. A collection derived from a custom field will +# have the name of the field added to the value. For example, if a custom series # column named 'My Series' contains the name 'Darkover', then the collection -# will be named 'Darkover (My Series)'. If two books have fields that generate -# the same collection name, then both books will be in that collection. This -# tweak lets you specify for a standard or custom field the value to be put -# inside the parentheses. You can use it to add a parenthetical description to a +# will by default be named 'Darkover (My Series)'. For purposes of this +# documentation, 'Darkover' is called the value and 'My Series' is called the +# category. If two books have fields that generate the same collection name, +# then both books will be in that collection. +# This set of tweaks tweak lets you specify for a standard or custom field how +# the collections are to be named. You can use it to add a description to a # standard field, for example 'Foo (Tag)' instead of the 'Foo'. You can also use # it to force multiple fields to end up in the same collection. For example, you # could force the values in 'series', '#my_series_1', and '#my_series_2' to # appear in collections named 'some_value (Series)', thereby merging all of the -# fields into one set of collections. The syntax of this tweak is -# {'field_lookup_name':'name_to_use', 'lookup_name':'name', ...} -# Example 1: I want three series columns to be merged into one set of -# collections. If the column lookup names are 'series', '#series_1' and -# '#series_2', and if I want nothing in the parenthesis, then the value to use -# in the tweak value would be: -# sony_collection_renaming_rules={'series':'', '#series_1':'', '#series_2':''} -# Example 2: I want the word '(Series)' to appear on collections made from -# series, and the word '(Tag)' to appear on collections made from tags. Use: -# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'} -# Example 3: I want 'series' and '#myseries' to be merged, and for the -# collection name to have '(Series)' appended. The renaming rule is: -# sony_collection_renaming_rules={'series':'Series', '#myseries':'Series'} +# fields into one set of collections. +# There are two related tweaks. The first determines the category name to use +# for a metadata field. The second is a template, used to determines how the +# value and category are combined to create the collection name. +# The syntax of the first tweak, sony_collection_renaming_rules, is: +# {'field_lookup_name':'category_name_to_use', 'lookup_name':'name', ...} +# The second tweak, sony_collection_name_template, is a template. It uses the +# same template language as plugboards and save templates. This tweak controls +# how the value and category are combined together to make the collection name. +# The only two fields available are {category} and {value}. The {value} field is +# never empty. The {category} field can be empty. The default is to put the +# value first, then the category enclosed in parentheses, it is isn't empty: +# '{value} {category:|(|)}' +# Examples: The first three examples assume that the second tweak +# has not been changed. +# 1: I want three series columns to be merged into one set of collections. The +# column lookup names are 'series', '#series_1' and '#series_2'. I want nothing +# in the parenthesis. The value to use in the tweak value would be: +# sony_collection_renaming_rules={'series':'', '#series_1':'', '#series_2':''} +# 2: I want the word '(Series)' to appear on collections made from series, and +# the word '(Tag)' to appear on collections made from tags. Use: +# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'} +# 3: I want 'series' and '#myseries' to be merged, and for the collection name +# to have '(Series)' appended. The renaming rule is: +# sony_collection_renaming_rules={'series':'Series', '#myseries':'Series'} +# 4: Same as example 2, but instead of having the category name in parentheses +# and appended to the value, I want it prepended and separated by a colon, such +# as in Series: Darkover. I must change the template used to format the category name +# The resulting two tweaks are: +# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'} +# sony_collection_name_template='{category:||: }{value}' sony_collection_renaming_rules={} +sony_collection_name_template='{value}{category:| (|)}' # Specify how sony collections are sorted. This tweak is only applicable if diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index ba005c4e6d..73afd770c1 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -14,6 +14,22 @@ from calibre.constants import preferred_encoding from calibre import isbytestring, force_unicode from calibre.utils.config import prefs, tweaks from calibre.utils.icu import strcmp +from calibre.utils.formatter import TemplateFormatter + +class SafeFormat(TemplateFormatter): + ''' + Provides a format function that substitutes '' for any missing value + ''' + + def get_value(self, key, args, kwargs): + try: + if key in kwargs: + return kwargs[key] + return key + except: + return key + +safe_formatter = SafeFormat() class Book(Metadata): def __init__(self, prefix, lpath, size=None, other=None): @@ -107,23 +123,25 @@ class CollectionsBookList(BookList): return sortattr return None - def compute_category_name(self, attr, category, field_meta): + def compute_category_name(self, field_key, field_value, field_meta): renames = tweaks['sony_collection_renaming_rules'] - attr_name = renames.get(attr, None) - if attr_name is None: + field_name = renames.get(field_key, None) + if field_name is None: if field_meta['is_custom']: - attr_name = '(%s)'%field_meta['name'] + field_name = field_meta['name'] else: - attr_name = '' - elif attr_name != '': - attr_name = '(%s)'%attr_name - cat_name = '%s %s'%(category, attr_name) + field_name = '' + cat_name = safe_formatter.safe_format( + fmt=tweaks['sony_collection_name_template'], + kwargs={'category':field_name, 'value':field_value}, + error_value='', book=None) return cat_name.strip() def get_collections(self, collection_attributes): from calibre.devices.usbms.driver import debug_print debug_print('Starting get_collections:', prefs['manage_device_metadata']) debug_print('Renaming rules:', tweaks['sony_collection_renaming_rules']) + debug_print('Formatting template:', tweaks['sony_collection_name_template']) debug_print('Sorting rules:', tweaks['sony_collection_sorting_rules']) # Complexity: we can use renaming rules only when using automatic From be1dc1059c97eab461f6e1dfd98551cdd9829454 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 24 Dec 2010 13:37:27 +0000 Subject: [PATCH 03/58] Enhancement #8032: Double click to edit metadata --- resources/default_tweaks.py | 6 ++++-- src/calibre/gui2/library/views.py | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index d01999e766..efcd004acd 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -265,8 +265,10 @@ generate_cover_title_font = None generate_cover_foot_font = None -# Behavior of doubleclick on the books list. Choices: -# open_viewer, do_nothing, edit_cell. Default: open_viewer. +# Behavior of doubleclick on the books list. Choices: open_viewer, do_nothing, +# edit_cell, edit_metadata. Selecting edit_metadata has the side effect of +# disabling editing a field using a single click. +# Default: open_viewer. # Example: doubleclick_on_library_view = 'do_nothing' doubleclick_on_library_view = 'open_viewer' diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index a6285c6656..8dad4c21b1 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -57,6 +57,11 @@ class BooksView(QTableView): # {{{ elif tweaks['doubleclick_on_library_view'] == 'open_viewer': self.setEditTriggers(self.SelectedClicked|self.editTriggers()) self.doubleClicked.connect(parent.iactions['View'].view_triggered) + elif tweaks['doubleclick_on_library_view'] == 'edit_metadata': + # Must not enable single-click to edit, or the field will remain + # open in edit mode underneath the edit metadata dialog + self.doubleClicked.connect( + partial(parent.iactions['Edit Metadata'].edit_metadata, checked=False)) self.drag_allowed = True self.setDragEnabled(True) From 737fc8961fe5129e8cc3adaec247786926c5f7c9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 24 Dec 2010 09:40:13 -0700 Subject: [PATCH 04/58] ... --- src/calibre/ebooks/mobi/mobiml.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 001cf2c1e9..9733c5f4ca 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -468,8 +468,9 @@ class MobiMLizer(object): vtag.append(child) else: break - for child in vbstate.para: - vtag.append(child) + if vbstate.para is not None: + for child in vbstate.para: + vtag.append(child) return if text or tag in CONTENT_TAGS or tag in NESTABLE_TAGS: From f293307e09f2222b0685dc5037cc74386e4c2974 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 24 Dec 2010 11:05:34 -0700 Subject: [PATCH 05/58] Fix #8042 (Request for Asus Eee-Reader DR900 Support) --- src/calibre/customize/builtins.py | 3 ++- src/calibre/devices/misc.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 93dda884cc..aea0e340c4 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -478,7 +478,7 @@ from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \ from calibre.devices.sne.driver import SNE from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \ GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR, \ - TREKSTOR + TREKSTOR, EEEREADER from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.kobo.driver import KOBO from calibre.devices.bambook.driver import BAMBOOK @@ -605,6 +605,7 @@ plugins += [ ALURATEK_COLOR, BAMBOOK, TREKSTOR, + EEEREADER, ITUNES, ] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index a895948316..d4776ecca7 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -244,3 +244,23 @@ class TREKSTOR(USBMS): VENDOR_NAME = 'TREKSTOR' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_PLAYER_7' +class EEEREADER(USBMS): + + name = 'Asus EEE Reader device interface' + gui_name = 'EEE Reader' + description = _('Communicate with the EEE Reader') + author = 'Kovid Goyal' + supported_platforms = ['windows', 'osx', 'linux'] + + # Ordered list of supported formats + FORMATS = ['epub', 'fb2', 'txt', 'pdf'] + + VENDOR_ID = [0x0b05] + PRODUCT_ID = [0x178f] + BCD = [0x0319] + + EBOOK_DIR_MAIN = 'Books' + + VENDOR_NAME = 'LINUX' + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET' + From 785c3b84308fb177be9380e3554e15f18dbcb951 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 24 Dec 2010 11:38:20 -0700 Subject: [PATCH 06/58] Fix #8045 (Viewer - Previous Page button broken) --- src/calibre/gui2/viewer/documentview.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 343d85e63e..f77f23c154 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -769,7 +769,7 @@ class DocumentView(QWebView): # {{{ self.to_bottom = True if epf: self.flipper.initialize(self.current_page_image(), False) - self.manager.previous_document() + self.manager.previous_document() else: opos = self.document.ypos upper_limit = opos - delta_y @@ -783,8 +783,8 @@ class DocumentView(QWebView): # {{{ if epf: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) - if self.manager is not None: - self.manager.scrolled(self.scroll_fraction) + if self.manager is not None: + self.manager.scrolled(self.scroll_fraction) def next_page(self): if self.flipper.running and not self.is_auto_repeat_event: From a11820ecfdb30e4486ecd6aebfc2bb15f954b240 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 24 Dec 2010 12:22:08 -0700 Subject: [PATCH 07/58] Comments editor: Use three rows for the buttons --- src/calibre/gui2/comments_editor.py | 52 +++++++++++++++++------------ 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 2d0d1c209e..97a218a10b 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -479,6 +479,7 @@ class Editor(QWidget): # {{{ QWidget.__init__(self, parent) self.toolbar1 = QToolBar(self) self.toolbar2 = QToolBar(self) + self.toolbar3 = QToolBar(self) self.editor = EditorWidget(self) self.tabs = QTabWidget(self) self.tabs.setTabPosition(self.tabs.South) @@ -493,6 +494,7 @@ class Editor(QWidget): # {{{ l.setContentsMargins(0, 0, 0, 0) l.addWidget(self.toolbar1) l.addWidget(self.toolbar2) + l.addWidget(self.toolbar3) l.addWidget(self.editor) self._layout.addWidget(self.tabs) self.tabs.addTab(self.wyswyg, _('Normal view')) @@ -500,19 +502,7 @@ class Editor(QWidget): # {{{ self.tabs.currentChanged[int].connect(self.change_tab) self.highlighter = Highlighter(self.code_edit.document()) - for x in ('bold', 'italic', 'underline', 'strikethrough', - 'superscript', 'subscript', 'indent', 'outdent'): - ac = getattr(self.editor, 'action_'+x) - if x in ('superscript', 'indent'): - self.toolbar2.addSeparator() - self.toolbar2.addAction(ac) - self.toolbar2.addSeparator() - - for x in ('left', 'center', 'right', 'justified'): - ac = getattr(self.editor, 'action_align_'+x) - self.toolbar2.addAction(ac) - self.toolbar2.addSeparator() - + # toolbar1 {{{ self.toolbar1.addAction(self.editor.action_undo) self.toolbar1.addAction(self.editor.action_redo) self.toolbar1.addAction(self.editor.action_select_all) @@ -523,21 +513,39 @@ class Editor(QWidget): # {{{ for x in ('copy', 'cut', 'paste'): ac = getattr(self.editor, 'action_'+x) self.toolbar1.addAction(ac) - self.toolbar1.addSeparator() + self.toolbar1.addSeparator() + self.toolbar1.addAction(self.editor.action_background) + # }}} + + # toolbar2 {{{ for x in ('', 'un'): ac = getattr(self.editor, 'action_%sordered_list'%x) - self.toolbar1.addAction(ac) - self.toolbar1.addSeparator() + self.toolbar2.addAction(ac) + self.toolbar2.addSeparator() + for x in ('superscript', 'subscript', 'indent', 'outdent'): + self.toolbar2.addAction(getattr(self.editor, 'action_' + x)) + if x in ('subscript', 'outdent'): + self.toolbar2.addSeparator() - self.toolbar1.addAction(self.editor.action_color) - self.toolbar1.addAction(self.editor.action_background) - self.toolbar1.addSeparator() - - self.toolbar1.addAction(self.editor.action_block_style) - w = self.toolbar1.widgetForAction(self.editor.action_block_style) + self.toolbar2.addAction(self.editor.action_block_style) + w = self.toolbar2.widgetForAction(self.editor.action_block_style) w.setPopupMode(w.InstantPopup) self.toolbar2.addAction(self.editor.action_insert_link) + # }}} + + # toolbar3 {{{ + for x in ('bold', 'italic', 'underline', 'strikethrough'): + ac = getattr(self.editor, 'action_'+x) + self.toolbar3.addAction(ac) + self.toolbar3.addSeparator() + + for x in ('left', 'center', 'right', 'justified'): + ac = getattr(self.editor, 'action_align_'+x) + self.toolbar3.addAction(ac) + self.toolbar3.addSeparator() + self.toolbar3.addAction(self.editor.action_color) + # }}} self.code_edit.textChanged.connect(self.code_dirtied) self.editor.page().contentsChanged.connect(self.wyswyg_dirtied) From b06596fffbeb4102ed573fee1366e345e64db6d9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 24 Dec 2010 22:44:04 +0000 Subject: [PATCH 08/58] First try at using the new comments editor for custom comments columns --- src/calibre/gui2/book_details.py | 4 +- src/calibre/gui2/custom_column_widgets.py | 9 +-- src/calibre/gui2/dialogs/comments_dialog.py | 5 +- src/calibre/gui2/dialogs/comments_dialog.ui | 37 ++++++----- src/calibre/gui2/dialogs/template_dialog.py | 25 +++++++ src/calibre/gui2/dialogs/template_dialog.ui | 73 +++++++++++++++++++++ src/calibre/gui2/library/delegates.py | 24 +++++-- src/calibre/gui2/library/models.py | 2 + 8 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 src/calibre/gui2/dialogs/template_dialog.py create mode 100644 src/calibre/gui2/dialogs/template_dialog.ui diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index a3caa82e4b..50ce72686a 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -44,7 +44,9 @@ def render_rows(data): key = key.decode(preferred_encoding, 'replace') if isinstance(txt, str): txt = txt.decode(preferred_encoding, 'replace') - if '' not in txt: + if key.endswith(u':html'): + key = key[:-5] + elif '' not in txt: txt = prepare_string_for_xml(txt) if 'id' in data: if key == _('Path'): diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index ca9243e51e..bf66ea7235 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -15,6 +15,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \ from calibre.utils.date import qt_to_dt, now from calibre.gui2.widgets import TagsLineEdit, EnComboBox +from calibre.gui2.comments_editor import Editor as CommentsEditor from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.utils.config import tweaks from calibre.utils.icu import sort_key @@ -186,9 +187,9 @@ class Comments(Base): self._box = QGroupBox(parent) self._box.setTitle('&'+self.col_metadata['name']) self._layout = QVBoxLayout() - self._tb = QPlainTextEdit(self._box) + self._tb = CommentsEditor(self._box) self._tb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) - self._tb.setTabChangesFocus(True) + #self._tb.setTabChangesFocus(True) self._layout.addWidget(self._tb) self._box.setLayout(self._layout) self.widgets = [self._box] @@ -196,10 +197,10 @@ class Comments(Base): def setter(self, val): if val is None: val = '' - self._tb.setPlainText(val) + self._tb.html = val def getter(self): - val = unicode(self._tb.toPlainText()).strip() + val = unicode(self._tb.html).strip() if not val: val = None return val diff --git a/src/calibre/gui2/dialogs/comments_dialog.py b/src/calibre/gui2/dialogs/comments_dialog.py index 5d53448b94..51b29fa989 100644 --- a/src/calibre/gui2/dialogs/comments_dialog.py +++ b/src/calibre/gui2/dialogs/comments_dialog.py @@ -5,6 +5,7 @@ __license__ = 'GPL v3' from PyQt4.Qt import Qt, QDialog, QDialogButtonBox from calibre.gui2.dialogs.comments_dialog_ui import Ui_CommentsDialog +from calibre.library.comments import comments_to_html class CommentsDialog(QDialog, Ui_CommentsDialog): @@ -18,8 +19,8 @@ class CommentsDialog(QDialog, Ui_CommentsDialog): self.setWindowIcon(icon) if text is not None: - self.textbox.setPlainText(text) - self.textbox.setTabChangesFocus(True) + self.textbox.html = comments_to_html(text) + # self.textbox.setTabChangesFocus(True) self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) diff --git a/src/calibre/gui2/dialogs/comments_dialog.ui b/src/calibre/gui2/dialogs/comments_dialog.ui index dccfa48652..9c6e6cb861 100644 --- a/src/calibre/gui2/dialogs/comments_dialog.ui +++ b/src/calibre/gui2/dialogs/comments_dialog.ui @@ -19,22 +19,29 @@ Edit Comments - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Editor + QWidget +

calibre/gui2/comments_editor.h
+ + diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py new file mode 100644 index 0000000000..aaa4e2bb9a --- /dev/null +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' +__license__ = 'GPL v3' + +from PyQt4.Qt import Qt, QDialog, QDialogButtonBox +from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog + +class TemplateDialog(QDialog, Ui_TemplateDialog): + + def __init__(self, parent, text): + QDialog.__init__(self, parent) + Ui_TemplateDialog.__init__(self) + self.setupUi(self) + # Remove help icon on title bar + icon = self.windowIcon() + self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) + self.setWindowIcon(icon) + + if text is not None: + self.textbox.setPlainText(text) + self.textbox.setTabChangesFocus(True) + self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) + self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) + diff --git a/src/calibre/gui2/dialogs/template_dialog.ui b/src/calibre/gui2/dialogs/template_dialog.ui new file mode 100644 index 0000000000..3eacace2c5 --- /dev/null +++ b/src/calibre/gui2/dialogs/template_dialog.ui @@ -0,0 +1,73 @@ + + + TemplateDialog + + + + 0 + 0 + 336 + 235 + + + + + 0 + 0 + + + + Edit Comments + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + TemplateDialog + accept() + + + 229 + 211 + + + 157 + 234 + + + + + buttonBox + rejected() + TemplateDialog + reject() + + + 297 + 217 + + + 286 + 234 + + + + + diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index fe7e7d55ba..2ae6cf2936 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -13,7 +13,7 @@ from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \ QPen, QStyle, QPainter, QStyleOptionViewItemV4, \ QIcon, QDoubleSpinBox, QVariant, QSpinBox, \ QStyledItemDelegate, QCompleter, \ - QComboBox + QComboBox, QTextDocument from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.gui2.widgets import EnLineEdit, TagsLineEdit @@ -22,6 +22,8 @@ from calibre.utils.config import tweaks from calibre.utils.formatter import validation_formatter from calibre.utils.icu import sort_key from calibre.gui2.dialogs.comments_dialog import CommentsDialog +from calibre.gui2.dialogs.template_dialog import TemplateDialog + class RatingDelegate(QStyledItemDelegate): # {{{ COLOR = QColor("blue") @@ -294,6 +296,20 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{ Delegate for comments data. ''' + def paint(self, painter, option, index): + document = QTextDocument() + value = index.data(Qt.DisplayRole) +# if value.isValid() and not value.isNull(): +# QString text("This is highlighted."); + text = value.toString() + document.setHtml(text); + painter.save() + painter.setClipRect(option.rect) + painter.translate(option.rect.topLeft()); + document.drawContents(painter); + painter.restore() +# painter.translate(-option.rect.topLeft()); + def createEditor(self, parent, option, index): m = index.model() col = m.column_map[index.column()] @@ -301,11 +317,11 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{ editor = CommentsDialog(parent, text) d = editor.exec_() if d: - m.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole) + m.setData(index, QVariant(editor.textbox.html), Qt.EditRole) return None def setModelData(self, editor, model, index): - model.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole) + model.setData(index, QVariant(editor.textbox.html), Qt.EditRole) # }}} class CcBoolDelegate(QStyledItemDelegate): # {{{ @@ -351,7 +367,7 @@ class CcTemplateDelegate(QStyledItemDelegate): # {{{ def createEditor(self, parent, option, index): m = index.model() text = m.custom_columns[m.column_map[index.column()]]['display']['composite_template'] - editor = CommentsDialog(parent, text) + editor = TemplateDialog(parent, text) editor.setWindowTitle(_("Edit template")) editor.textbox.setTabChangesFocus(False) editor.textbox.setTabStopWidth(20) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 9da9a2f538..920753a77d 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -334,6 +334,8 @@ class BooksModel(QAbstractTableModel): # {{{ if key not in cf_to_display: continue name, val = mi.format_field(key) + if mi.metadata_for_field(key)['datatype'] == 'comments': + name += ':html' if val: data[name] = val return data From 1fa911c8c962642b611145292776e2c3af37becb Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 24 Dec 2010 22:46:42 +0000 Subject: [PATCH 09/58] Resize the comments delegate dialog box --- src/calibre/gui2/dialogs/comments_dialog.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/comments_dialog.ui b/src/calibre/gui2/dialogs/comments_dialog.ui index 9c6e6cb861..76b23e233d 100644 --- a/src/calibre/gui2/dialogs/comments_dialog.ui +++ b/src/calibre/gui2/dialogs/comments_dialog.ui @@ -6,8 +6,8 @@ 0 0 - 336 - 235 + 400 + 400 From 35bfaab6699963578d11a6aa527f7802ae4df5a4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 24 Dec 2010 15:47:56 -0700 Subject: [PATCH 10/58] Fix #8047 (Saved Searches not working in Web Browser) --- src/calibre/library/server/browse.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index b1c4a4c2f9..afc20ba21c 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -556,18 +556,19 @@ class BrowseServer(object): ids = self.search_cache('search:"%s"'%which) except: raise cherrypy.HTTPError(404, 'Search: %r not understood'%which) - all_ids = self.search_cache('') - if category == 'newest': - ids = all_ids - hide_sort = 'true' - elif category == 'allbooks': - ids = all_ids else: - q = category - if q == 'news': - q = 'tags' - ids = self.db.get_books_for_category(q, cid) - ids = [x for x in ids if x in all_ids] + all_ids = self.search_cache('') + if category == 'newest': + ids = all_ids + hide_sort = 'true' + elif category == 'allbooks': + ids = all_ids + else: + q = category + if q == 'news': + q = 'tags' + ids = self.db.get_books_for_category(q, cid) + ids = [x for x in ids if x in all_ids] items = [self.db.data._data[x] for x in ids] if category == 'newest': From c0c4df77cab4ddddb009743546978e08e8490d69 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 25 Dec 2010 11:42:38 +0000 Subject: [PATCH 11/58] Improve search/replace handling of is_multiple fields --- src/calibre/gui2/dialogs/metadata_bulk.py | 33 ++++++-- src/calibre/gui2/dialogs/metadata_bulk.ui | 99 +++++++++++++++++------ 2 files changed, 101 insertions(+), 31 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index bde5cae128..9dbc3dee5e 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -417,6 +417,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.multiple_separator.setFixedWidth(30) self.multiple_separator.setText(' ::: ') self.multiple_separator.textChanged.connect(self.s_r_separator_changed) + self.results_count.valueChanged[int].connect(self.s_r_display_bounds_changed) + self.starting_from.valueChanged[int].connect(self.s_r_display_bounds_changed) def s_r_get_field(self, mi, field): if field: @@ -439,6 +441,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): val = [] return val + def s_r_display_bounds_changed(self, i): + self.s_r_search_field_changed(self.search_field.currentIndex()) + def s_r_template_changed(self): self.s_r_search_field_changed(self.search_field.currentIndex()) @@ -454,6 +459,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): mi = self.db.get_metadata(self.ids[i], index_is_id=True) src = unicode(self.search_field.currentText()) t = self.s_r_get_field(mi, src) + if len(t) > 1: + t = t[self.starting_from.value()-1: + self.starting_from.value()-1 + self.results_count.value()] w.setText(unicode(self.multiple_separator.text()).join(t)) if self.search_mode.currentIndex() == 0: @@ -466,12 +474,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): txt = unicode(txt) if not txt: txt = unicode(self.search_field.currentText()) - self.comma_separated.setEnabled(True) if txt and txt in self.writable_fields: self.destination_field_fm = self.db.metadata_for_field(txt) - if self.destination_field_fm['is_multiple']: - self.comma_separated.setEnabled(False) - self.comma_separated.setChecked(True) self.s_r_paint_results(None) def s_r_search_mode_changed(self, val): @@ -542,6 +546,22 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): dest = src dest_mode = self.replace_mode.currentIndex() + if self.destination_field_fm['is_multiple']: + if self.comma_separated.isChecked(): + if dest == 'authors': + splitter = ' & ' + else: + splitter = ',' + + res = [] + for v in val: + for x in v.split(splitter): + if x.strip(): + res.append(x.strip()) + val = res + else: + val = [v.replace(',', '') for v in val] + if dest_mode != 0: dest_val = mi.get(dest, '') if dest_val is None: @@ -602,8 +622,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): try: result = self.s_r_do_regexp(mi) t = self.s_r_do_destination(mi, result) - if len(result) > 1 and self.destination_field_fm is not None and \ - self.destination_field_fm['is_multiple']: + if len(t) > 1 and self.destination_field_fm['is_multiple']: + t = t[self.starting_from.value()-1: + self.starting_from.value()-1 + self.results_count.value()] t = unicode(self.multiple_separator.text()).join(t) else: t = self.s_r_replace_mode_separator().join(t) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index d945909f96..41858b099b 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -658,11 +658,12 @@ If blank, the source field is used if the field is modifiable - Specifies whether a comma should be put between values when copying from a -multiple-valued field to a single-valued field + Specifies whether result items should be split into multiple values or +left as single values. This option has the most effect when the source field is +not multiple and the destination field is multiple - &Use comma + Split &result true @@ -684,28 +685,8 @@ multiple-valued field to a single-valued field - - - - Test text - - - test_text - - - - + - - - - Test result - - - test_result - - - @@ -719,10 +700,62 @@ multiple-valued field to a single-valued field + + + + For multiple-valued fields, sho&w + + + results_count + + + + + + + true + + + 1 + + + 999 + + + 999 + + + + + + + values starting a&t + + + starting_from + + + + + + + true + + + 1 + + + 999 + + + 1 + + + - Multi&ple separator: + with values separated b&y multiple_separator @@ -756,6 +789,20 @@ multiple-valued field to a single-valued field + + + + Test text + + + + + + + Test result + + + @@ -857,6 +904,8 @@ multiple-valued field to a single-valued field destination_field replace_mode comma_separated + results_count + starting_from multiple_separator test_text test_result From 092982e057272e57819310dbb53cf2bb2ccce286 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 25 Dec 2010 12:40:55 +0000 Subject: [PATCH 12/58] More custom comments as html work --- src/calibre/gui2/dialogs/book_info.py | 5 ++++- src/calibre/gui2/library/delegates.py | 18 +++++++++--------- src/calibre/library/server/opds.py | 2 ++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 016f132c57..6cae27d926 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -12,6 +12,7 @@ from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo from calibre.gui2 import dynamic, open_local_file from calibre import fit_image from calibre.library.comments import comments_to_html +from calibre.utils.icu import sort_key class BookInfo(QDialog, Ui_BookInfo): @@ -130,9 +131,11 @@ class BookInfo(QDialog, Ui_BookInfo): for f in formats: f = f.strip() info[_('Formats')] += '%s, '%(f,f) - for key in info.keys(): + for key in sorted(info.keys(), key=sort_key): if key == 'id': continue txt = info[key] + if key.endswith(':html'): + key = key[:-5] if key != _('Path'): txt = u'
\n'.join(textwrap.wrap(txt, 120)) rows += u'%s:%s'%(key, txt) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index 2ae6cf2936..957828f93c 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -296,19 +296,19 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{ Delegate for comments data. ''' + def __init__(self, parent): + QStyledItemDelegate.__init__(self, parent) + self.document = QTextDocument() + def paint(self, painter, option, index): - document = QTextDocument() - value = index.data(Qt.DisplayRole) -# if value.isValid() and not value.isNull(): -# QString text("This is highlighted."); - text = value.toString() - document.setHtml(text); + self.document.setHtml(index.data(Qt.DisplayRole).toString()) painter.save() + if option.state & QStyle.State_Selected: + painter.fillRect(option.rect, option.palette.highlight()) painter.setClipRect(option.rect) - painter.translate(option.rect.topLeft()); - document.drawContents(painter); + painter.translate(option.rect.topLeft()) + self.document.drawContents(painter) painter.restore() -# painter.translate(-option.rect.topLeft()); def createEditor(self, parent, option, index): m = index.model() diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index e447c6966c..fd8c50c594 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -173,6 +173,8 @@ def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS, prefix): extra.append('%s: %s
'%(xml(name), xml(format_tag_string(val, ',', ignore_max=True, no_tag_count=True)))) + elif datatype == 'comments': + extra.append('%s: %s
'%(xml(name), comments_to_html(unicode(val)))) else: extra.append('%s: %s
'%(xml(name), xml(unicode(val)))) comments = item[FM['comments']] From e770af4d47d72792068311e8244d7762b581822b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 25 Dec 2010 18:08:11 +0000 Subject: [PATCH 13/58] New formatter function, fix problems with strcat and math results, define basic formatter for usbms and formatter --- src/calibre/devices/usbms/books.py | 21 +++------------- src/calibre/utils/formatter.py | 40 +++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 73afd770c1..1e7d74480a 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -14,22 +14,7 @@ from calibre.constants import preferred_encoding from calibre import isbytestring, force_unicode from calibre.utils.config import prefs, tweaks from calibre.utils.icu import strcmp -from calibre.utils.formatter import TemplateFormatter - -class SafeFormat(TemplateFormatter): - ''' - Provides a format function that substitutes '' for any missing value - ''' - - def get_value(self, key, args, kwargs): - try: - if key in kwargs: - return kwargs[key] - return key - except: - return key - -safe_formatter = SafeFormat() +from calibre.utils.formatter import eval_formatter class Book(Metadata): def __init__(self, prefix, lpath, size=None, other=None): @@ -131,10 +116,10 @@ class CollectionsBookList(BookList): field_name = field_meta['name'] else: field_name = '' - cat_name = safe_formatter.safe_format( + cat_name = eval_formatter.safe_format( fmt=tweaks['sony_collection_name_template'], kwargs={'category':field_name, 'value':field_value}, - error_value='', book=None) + error_value='GET_CATEGORY', book=None) return cat_name.strip() def get_collections(self, collection_attributes): diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 182aff5a7a..bb7e953d19 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -36,7 +36,7 @@ class _Parser(object): return gt def _assign(self, target, value): - setattr(self, target, value) + self.variables[target] = value return value def _concat(self, *args): @@ -55,18 +55,23 @@ class _Parser(object): } x = float(x if x else 0) y = float(y if y else 0) - return ops[op](x, y) + return str(ops[op](x, y)) def _template(self, template): template = template.replace('[[', '{').replace(']]', '}') return self.parent.safe_format(template, self.parent.kwargs, 'TEMPLATE', self.parent.book) + def _eval(self, template): + template = template.replace('[[', '{').replace(']]', '}') + return eval_formatter.safe_format(template, self.variables, 'EVAL', None) + local_functions = { 'add' : (2, partial(_math, op='+')), 'assign' : (2, _assign), 'cmp' : (5, _cmp), 'divide' : (2, partial(_math, op='/')), + 'eval' : (1, _eval), 'field' : (1, lambda s, x: s.parent.get_value(x, [], s.parent.kwargs)), 'multiply' : (2, partial(_math, op='*')), 'strcat' : (-1, _concat), @@ -82,7 +87,7 @@ class _Parser(object): if prog[1] != '': self.error(_('failed to scan program. Invalid input {0}').format(prog[1])) self.parent = parent - setattr(self, '$', val) + self.variables = {'$':val} def error(self, message): m = 'Formatter: ' + message + _(' near ') @@ -144,7 +149,7 @@ class _Parser(object): # We have an identifier. Determine if it is a function id = self.token() if not self.token_op_is_a('('): - return getattr(self, id, _('unknown id ') + id) + return self.variables.get(id, _('unknown id ') + id) # We have a function. # Check if it is a known one. We do this here so error reporting is # better, as it can identify the tokens near the problem. @@ -417,15 +422,18 @@ class TemplateFormatter(string.Formatter): self.kwargs = kwargs self.book = book self.composite_values = {} - try: - ans = self.vformat(fmt, [], kwargs).strip() - except Exception, e: - if DEBUG: - traceback.print_exc() - ans = error_value + ' ' + e.message + if fmt.startswith('program:'): + ans = self._eval_program(None, fmt[8:]) + else: + try: + ans = self.vformat(fmt, [], kwargs).strip() + except Exception, e: + if DEBUG: + traceback.print_exc() + ans = error_value + ' ' + e.message return ans -class ValidateFormat(TemplateFormatter): +class ValidateFormatter(TemplateFormatter): ''' Provides a format function that substitutes '' for any missing value ''' @@ -435,6 +443,14 @@ class ValidateFormat(TemplateFormatter): def validate(self, x): return self.vformat(x, [], {}) -validation_formatter = ValidateFormat() +validation_formatter = ValidateFormatter() +class EvalFormatter(TemplateFormatter): + ''' + A template formatter that uses a simple dict instead of an mi instance + ''' + def get_value(self, key, args, kwargs): + return kwargs.get(key, _('No such variable ') + key) + +eval_formatter = EvalFormatter() From 6e80dca1bbd1b7925409571d034c8353179e5108 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 25 Dec 2010 11:24:50 -0700 Subject: [PATCH 14/58] ... --- src/calibre/gui2/library/delegates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index fef1542737..b41fd78dc3 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -306,7 +306,7 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{ painter.save() if hasattr(QStyle, 'CE_ItemViewItem'): style.drawControl(QStyle.CE_ItemViewItem, option, - painter, self._parent) + painter, self.parent()) elif option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) painter.setClipRect(option.rect) From 532749201a15611d262e903409ad5ea3352a6083 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 25 Dec 2010 18:30:50 +0000 Subject: [PATCH 15/58] Make custom_column_widgets.py, book_info.py, and book_details.py use comments_to_html. Fix _parent to parent() --- src/calibre/gui2/book_details.py | 1 + src/calibre/gui2/custom_column_widgets.py | 3 ++- src/calibre/gui2/dialogs/book_info.py | 1 + src/calibre/gui2/library/delegates.py | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 50ce72686a..dd12080d7f 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -46,6 +46,7 @@ def render_rows(data): txt = txt.decode(preferred_encoding, 'replace') if key.endswith(u':html'): key = key[:-5] + txt = comments_to_html(txt) elif '' not in txt: txt = prepare_string_for_xml(txt) if 'id' in data: diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 40abb05f89..ec18675359 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -19,6 +19,7 @@ from calibre.gui2.comments_editor import Editor as CommentsEditor from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.utils.config import tweaks from calibre.utils.icu import sort_key +from calibre.library.comments import comments_to_html class Base(object): @@ -197,7 +198,7 @@ class Comments(Base): def setter(self, val): if val is None: val = '' - self._tb.html = val + self._tb.html = comments_to_html(val) def getter(self): val = unicode(self._tb.html).strip() diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 6cae27d926..1384c27b8c 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -136,6 +136,7 @@ class BookInfo(QDialog, Ui_BookInfo): txt = info[key] if key.endswith(':html'): key = key[:-5] + txt = comments_to_html(txt) if key != _('Path'): txt = u'
\n'.join(textwrap.wrap(txt, 120)) rows += u'%s:%s'%(key, txt) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index fef1542737..b41fd78dc3 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -306,7 +306,7 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{ painter.save() if hasattr(QStyle, 'CE_ItemViewItem'): style.drawControl(QStyle.CE_ItemViewItem, option, - painter, self._parent) + painter, self.parent()) elif option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) painter.setClipRect(option.rect) From efd609d81e15c5a4a45076a25be95243bbb63359 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 26 Dec 2010 12:34:52 +0000 Subject: [PATCH 16/58] Improvements to the template processor. Document new features. --- src/calibre/gui2/dialogs/template_dialog.py | 2 +- src/calibre/gui2/dialogs/template_dialog.ui | 2 +- src/calibre/manual/template_lang.rst | 100 +++++++++++++++++++- src/calibre/utils/formatter.py | 28 ++++-- 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index aaa4e2bb9a..60d4025ef9 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -19,7 +19,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): if text is not None: self.textbox.setPlainText(text) - self.textbox.setTabChangesFocus(True) + self.textbox.setTabStopWidth(50) self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) diff --git a/src/calibre/gui2/dialogs/template_dialog.ui b/src/calibre/gui2/dialogs/template_dialog.ui index 3eacace2c5..a30d6ef273 100644 --- a/src/calibre/gui2/dialogs/template_dialog.ui +++ b/src/calibre/gui2/dialogs/template_dialog.ui @@ -6,7 +6,7 @@ 0 0 - 336 + 500 235
diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index 0f3e543bee..6a4fef983f 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -137,8 +137,8 @@ Note that you can use the prefix and suffix as well. If you want the number to a {#myint:0>3s:ifempty(0)|[|]} -Using functions in templates - program mode -------------------------------------------- +Using functions in templates - template program mode +---------------------------------------------------- The template language program mode differs from single-function mode in that it permits you to write template expressions that refer to other metadata fields, modify values, and do arithmetic. It is a reasonably complete programming language. @@ -161,10 +161,13 @@ The syntax of the language is shown by the following grammar:: constant ::= " string " | ' string ' | number identifier ::= sequence of letters or ``_`` characters function ::= identifier ( statement [ , statement ]* ) - expression ::= identifier | constant | function + expression ::= identifier | constant | function | assignment + assignment ::= identifier '=' expression statement ::= expression [ ; expression ]* program ::= statement +Comments are lines with a '#' character at the beginning of the line. + An ``expression`` always has a value, either the value of the constant, the value contained in the identifier, or the value returned by a function. The value of a ``statement`` is the value of the last expression in the sequence of statements. As such, the value of the program (statement):: 1; 2; 'foobar'; 3 @@ -208,13 +211,102 @@ The following functions are available in addition to those described in single-f * ``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``. + * ``eval(string)`` -- evaluates the string as a program, passing the local variables (those `assign`ed to). This permits using the template processor to construct complex results from local variables. * ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers. + * ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole. * ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments. * ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``. * ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers. * ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value. - + +Using general program mode +----------------------------------- + +For more complicated template programs, it is sometimes easier to avoid template syntax (all the `{` and `}` characters), instead writing a more classical-looking program. You can do this in |app| by beginning the template with `program:`. In this case, no template processing is done. The special variable `$` is not set. It is up to your program to produce the correct results. + +One advantage of `program:` mode is that the brackets are no longer special. For example, it is not necessary to use `[[` and `]]` when using the `template()` function. + +The following example is a `program:` mode implementation of a recipe on the MobileRead forum: "Put series into the title, using either initials or a shortened form. Strip leading articles from the series name (any)." For example, for the book The Two Towers in the Lord of the Rings series, the recipe gives `LotR [02] The Two Towers`. Using standard templates, the recipe requires three custom columns and a plugboard, as explained in the following: + +The solution requires creating three composite columns. The first column is used to remove the leading articles. The second is used to compute the 'shorten' form. The third is to compute the 'initials' form. Once you have these columns, the plugboard selects between them. You can hide any or all of the three columns on the library view. + + First column: + Name: #stripped_series. + Template: {series:re(^(A|The|An)\s+,)||} + + Second column (the shortened form): + Name: #shortened. + Template: {#stripped_series:shorten(4,-,4)} + + Third column (the initials form): + Name: #initials. + Template: {#stripped_series:re(([^\s])[^\s]+(\s|$),\1)} + + Plugboard expression: + Template:{#stripped_series:lookup(.\s,#initials,.,#shortened,series)}{series_index:0>2.0f| [|] }{title} + Destination field: title + + This set of fields and plugboard produces: + Series: The Lord of the Rings + Series index: 2 + Title: The Two Towers + Output: LotR [02] The Two Towers + + Series: Dahak + Series index: 1 + Title: Mutineers Moon + Output: Dahak [01] Mutineers Moon + + Series: Berserkers + Series Index: 4 + Title: Berserker Throne + Output: Bers-kers [04] Berserker Throne + + Series: Meg Langslow Mysteries + Series Index: 3 + Title: Revenge of the Wrought-Iron Flamingos + Output: MLM [03] Revenge of the Wrought-Iron Flamingos + +The following program produces the same results as the original recipe, using only one custom column to hold the results of a program that computes the special title value. + + Custom column: + Name: #special_title + Template: (the following with all leading spaces removed) + program: + # compute the equivalent of the composit fields and store them in local variables + stripped = re(field('series'), '^(A|The|An)\s+', ''); + shortened = shorten(stripped, 4, '-' ,4); + initials = re(stripped, '[^\w]*(\w?)[^\s]+(\s|$)', '\1'); + + # Format the series index. Ends up as empty if there is no series index. + # Note that leading and trailing spaces will be removed by the formatter, + # so we cannot add them here. We will do that in the strcat below. + # Also note that because we are in 'program' mode, we can freely use + # curly brackets in strings, something we cannot do in template mode. + s_index = template('{series_index:0>2.0f}'); + + # print(stripped, shortened, initials, s_index); + + # Now concatenate all the bits together. The switch picks between + # initials and shortened, depending on whether there is a space + # in stripped. We then add the brackets around s_index if it is + # not empty. Finally, add the title. As this is the last function in + # the program, its value will be returned. + strcat( + switch( stripped, + '.\s', initials, + '.', shortened, + field('series')), + test(s_index, strcat(' [', s_index, '] '), ''), + field('title')); + + Plugboard expression: + Template:{#special_title} + Destination field: title + +It would be possible to do the above with no custom columns by putting the program into the template box of the plugboard. However, to do so, all comments must be removed because the plugboard text box does not support multi-line editing. It is debatable whether the gain of not having the custom column is worth the vast increase in difficulty caused by the program being one giant line. + Special notes for save/send templates ------------------------------------- diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 8936befa95..7587a334e8 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -66,6 +66,10 @@ class _Parser(object): template = template.replace('[[', '{').replace(']]', '}') return eval_formatter.safe_format(template, self.variables, 'EVAL', None) + def _print(self, *args): + print args + return None + local_functions = { 'add' : (2, partial(_math, op='+')), 'assign' : (2, _assign), @@ -74,6 +78,7 @@ class _Parser(object): 'eval' : (1, _eval), 'field' : (1, lambda s, x: s.parent.get_value(x, [], s.parent.kwargs)), 'multiply' : (2, partial(_math, op='*')), + 'print' : (-1, _print), 'strcat' : (-1, _concat), 'strcmp' : (5, _strcmp), 'substr' : (3, lambda s, x, y, z: x[int(y): len(x) if int(z) == 0 else int(z)]), @@ -143,12 +148,18 @@ class _Parser(object): if not self.token_op_is_a(';'): return val self.consume() + if self.token_is_eof(): + return val def expr(self): if self.token_is_id(): # We have an identifier. Determine if it is a function id = self.token() if not self.token_op_is_a('('): + if self.token_op_is_a('='): + # classic assignment statement + self.consume() + return self._assign(id, self.expr()) return self.variables.get(id, _('unknown id ') + id) # We have a function. # Check if it is a known one. We do this here so error reporting is @@ -339,6 +350,7 @@ class TemplateFormatter(string.Formatter): (r'\w+', lambda x,t: (2, t)), (r'".*?((? Date: Sun, 26 Dec 2010 14:24:22 +0000 Subject: [PATCH 17/58] Correct typo in the manual --- src/calibre/manual/template_lang.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index 6a4fef983f..b859a84340 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -274,7 +274,7 @@ The following program produces the same results as the original recipe, using on Name: #special_title Template: (the following with all leading spaces removed) program: - # compute the equivalent of the composit fields and store them in local variables + # compute the equivalent of the composite fields and store them in local variables stripped = re(field('series'), '^(A|The|An)\s+', ''); shortened = shorten(stripped, 4, '-' ,4); initials = re(stripped, '[^\w]*(\w?)[^\s]+(\s|$)', '\1'); From 30050f00334c855e9f432f7a0ceb581cd4e5eee2 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 26 Dec 2010 21:35:31 -0500 Subject: [PATCH 18/58] PDF Output: Simplify building printer object. Fix regessions that prevented options such as margins from being honored. --- src/calibre/ebooks/pdf/writer.py | 37 +++++++------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index 03519a2cbb..2ae1638a73 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -25,10 +25,6 @@ from PyQt4.QtWebKit import QWebView from pyPdf import PdfFileWriter, PdfFileReader -def get_pdf_printer(): - return QPrinter(QPrinter.HighResolution) - - def get_custom_size(opts): custom_size = None if opts.custom_size != None: @@ -42,12 +38,12 @@ def get_custom_size(opts): custom_size = None return custom_size -def setup_printer(opts, for_comic=False): +def get_pdf_printer(opts, for_comic=False): from calibre.gui2 import is_ok_to_use_qt if not is_ok_to_use_qt(): raise Exception('Not OK to use Qt') - printer = get_pdf_printer() + printer = QPrinter(QPrinter.HighResolution) custom_size = get_custom_size(opts) if opts.output_profile.short_name == 'default': @@ -63,13 +59,14 @@ def setup_printer(opts, for_comic=False): dpi = opts.output_profile.dpi printer.setPaperSize(QSizeF(float(w) / dpi, float(h)/dpi), QPrinter.Inch) - printer.setPageMargins(0, 0, 0, 0, QPrinter.Point) + printer.setPageMargins(opts.margin_left, opts.margin_top, opts.margin_right, opts.margin_bottom, QPrinter.Point) printer.setOrientation(orientation(opts.orientation)) printer.setOutputFormat(QPrinter.PdfFormat) + printer.setFullPage(True) return printer def get_printer_page_size(opts, for_comic=False): - printer = setup_printer(opts, for_comic=for_comic) + printer = get_pdf_printer(opts, for_comic=for_comic) size = printer.paperSize(QPrinter.Millimeter) return size.width() / 10., size.height() / 10. @@ -154,24 +151,11 @@ class PDFWriter(QObject): # {{{ self.view.load(QUrl.fromLocalFile(item)) - def get_printer(self, set_horz_margins=False): - printer = get_pdf_printer() - printer.setPaperSize(QSizeF(self.size[0] * 10, self.size[1] * 10), QPrinter.Millimeter) - if set_horz_margins: - printer.setPageMargins(0., self.opts.margin_top, 0., - self.opts.margin_bottom, QPrinter.Point) - else: - printer.setPageMargins(0, 0, 0, 0, QPrinter.Point) - printer.setOrientation(orientation(self.opts.orientation)) - printer.setOutputFormat(QPrinter.PdfFormat) - printer.setFullPage(not set_horz_margins) - return printer - def _render_html(self, ok): if ok: item_path = os.path.join(self.tmp_path, '%i.pdf' % len(self.combine_queue)) - self.logger.debug('\tRendering item %s as %i' % (os.path.basename(str(self.view.url().toLocalFile())), len(self.combine_queue))) - printer = self.get_printer(set_horz_margins=True) + self.logger.debug('\tRendering item %s as %i.pdf' % (os.path.basename(str(self.view.url().toLocalFile())), len(self.combine_queue))) + printer = get_pdf_printer(self.opts) printer.setOutputFileName(item_path) self.view.print_(printer) self._render_book() @@ -233,16 +217,11 @@ class ImagePDFWriter(object): os.remove(f.name) def render_images(self, outpath, mi, items): - printer = get_pdf_printer() - printer.setPaperSize(QSizeF(self.size[0] * 10, self.size[1] * 10), QPrinter.Millimeter) - printer.setPageMargins(0, 0, 0, 0, QPrinter.Point) - printer.setOrientation(orientation(self.opts.orientation)) - printer.setOutputFormat(QPrinter.PdfFormat) + printer = get_pdf_printer(self.opts) printer.setOutputFileName(outpath) printer.setDocName(mi.title) printer.setCreator(u'%s [%s]'%(__appname__, __version__)) # Seems to be no way to set author - printer.setFullPage(True) painter = QPainter(printer) painter.setRenderHints(QPainter.Antialiasing|QPainter.SmoothPixmapTransform) From 4e2f5ee60e9d99944cd951838a7a2ba63b157e6c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Dec 2010 00:09:43 -0700 Subject: [PATCH 19/58] Fix specially for the beloved maintainer of poppler --- COPYRIGHT | 5 +++++ src/calibre/ebooks/pdf/fonts.cpp | 2 +- src/calibre/ebooks/pdf/fonts.h | 2 +- src/calibre/ebooks/pdf/images.cpp | 7 +++++++ src/calibre/ebooks/pdf/images.h | 7 +++++++ src/calibre/ebooks/pdf/links.cpp | 2 +- src/calibre/ebooks/pdf/links.h | 2 +- src/calibre/ebooks/pdf/main.cpp | 7 +++++++ src/calibre/ebooks/pdf/reflow.cpp | 2 +- src/calibre/ebooks/pdf/reflow.h | 2 +- src/calibre/ebooks/pdf/utils.h | 2 +- 11 files changed, 33 insertions(+), 7 deletions(-) diff --git a/COPYRIGHT b/COPYRIGHT index a31d1dbcda..8790fb69dd 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -4,6 +4,11 @@ License: GPL-3 The full text of the GPL is distributed as in /usr/share/common-licenses/GPL-3 on Debian systems. +Files: src/calibre/ebooks/pdf/*.h,*.cpp +License: GPL-2 or later + The full text of the GPL is distributed as in + /usr/share/common-licenses/GPL-2 on Debian systems. + Files: src/calibre/ebooks/BeautifulSoup.py Copyright: Copyright (c) 2004-2007, Leonard Richardson License: BSD diff --git a/src/calibre/ebooks/pdf/fonts.cpp b/src/calibre/ebooks/pdf/fonts.cpp index 3cd7ef0c5b..99ab7517c1 100644 --- a/src/calibre/ebooks/pdf/fonts.cpp +++ b/src/calibre/ebooks/pdf/fonts.cpp @@ -1,6 +1,6 @@ /** * Copyright 2009 Kovid Goyal - * License: GNU GPL v3 + * License: GNU GPL v2+ */ diff --git a/src/calibre/ebooks/pdf/fonts.h b/src/calibre/ebooks/pdf/fonts.h index 55202c9573..1b380e1b87 100644 --- a/src/calibre/ebooks/pdf/fonts.h +++ b/src/calibre/ebooks/pdf/fonts.h @@ -1,6 +1,6 @@ /** * Copyright 2009 Kovid Goyal - * License: GNU GPL v3 + * License: GNU GPL v2+ */ diff --git a/src/calibre/ebooks/pdf/images.cpp b/src/calibre/ebooks/pdf/images.cpp index b3b062e1f4..4cd1ace776 100644 --- a/src/calibre/ebooks/pdf/images.cpp +++ b/src/calibre/ebooks/pdf/images.cpp @@ -1,3 +1,10 @@ +/** + * Copyright 2009 Kovid Goyal + * License: GNU GPL v2+ + */ + + + #include #include #include diff --git a/src/calibre/ebooks/pdf/images.h b/src/calibre/ebooks/pdf/images.h index 7d6f143147..1b4d9b58bf 100644 --- a/src/calibre/ebooks/pdf/images.h +++ b/src/calibre/ebooks/pdf/images.h @@ -1,3 +1,10 @@ +/** + * Copyright 2009 Kovid Goyal + * License: GNU GPL v2+ + */ + + + #pragma once #include diff --git a/src/calibre/ebooks/pdf/links.cpp b/src/calibre/ebooks/pdf/links.cpp index 414ff5ce24..8d28492bab 100644 --- a/src/calibre/ebooks/pdf/links.cpp +++ b/src/calibre/ebooks/pdf/links.cpp @@ -1,6 +1,6 @@ /** * Copyright 2009 Kovid Goyal - * License: GNU GPL v3 + * License: GNU GPL v2+ */ diff --git a/src/calibre/ebooks/pdf/links.h b/src/calibre/ebooks/pdf/links.h index a8a3127a77..c43911ddca 100644 --- a/src/calibre/ebooks/pdf/links.h +++ b/src/calibre/ebooks/pdf/links.h @@ -1,6 +1,6 @@ /** * Copyright 2009 Kovid Goyal - * License: GNU GPL v3 + * License: GNU GPL v2+ */ diff --git a/src/calibre/ebooks/pdf/main.cpp b/src/calibre/ebooks/pdf/main.cpp index 44257b50f5..4e6ec60388 100644 --- a/src/calibre/ebooks/pdf/main.cpp +++ b/src/calibre/ebooks/pdf/main.cpp @@ -1,3 +1,10 @@ +/** + * Copyright 2009 Kovid Goyal + * License: GNU GPL v2+ + */ + + + #ifndef PDF2XML #define UNICODE #define PY_SSIZE_T_CLEAN diff --git a/src/calibre/ebooks/pdf/reflow.cpp b/src/calibre/ebooks/pdf/reflow.cpp index c08d7e5507..0c569fe0d1 100644 --- a/src/calibre/ebooks/pdf/reflow.cpp +++ b/src/calibre/ebooks/pdf/reflow.cpp @@ -1,6 +1,6 @@ /** * Copyright 2009 Kovid Goyal - * License: GNU GPL v3 + * License: GNU GPL v2+ */ #include diff --git a/src/calibre/ebooks/pdf/reflow.h b/src/calibre/ebooks/pdf/reflow.h index deb1dec326..ad4b79929d 100644 --- a/src/calibre/ebooks/pdf/reflow.h +++ b/src/calibre/ebooks/pdf/reflow.h @@ -1,6 +1,6 @@ /** * Copyright 2009 Kovid Goyal - * License: GNU GPL v3 + * License: GNU GPL v2+ * Based on pdftohtml from the poppler project. */ diff --git a/src/calibre/ebooks/pdf/utils.h b/src/calibre/ebooks/pdf/utils.h index 43f435b1e3..4246239ac7 100644 --- a/src/calibre/ebooks/pdf/utils.h +++ b/src/calibre/ebooks/pdf/utils.h @@ -1,6 +1,6 @@ /** * Copyright 2009 Kovid Goyal - * License: GNU GPL v3 + * License: GNU GPL v2+ */ From c0645635ec0d63774fb8dd20a17d5bd58e8df578 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 27 Dec 2010 09:18:46 +0000 Subject: [PATCH 20/58] Add the iRiver Story WiFi --- src/calibre/devices/iriver/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/iriver/driver.py b/src/calibre/devices/iriver/driver.py index 10945f17cc..0ad540f8a3 100644 --- a/src/calibre/devices/iriver/driver.py +++ b/src/calibre/devices/iriver/driver.py @@ -20,11 +20,11 @@ class IRIVER_STORY(USBMS): FORMATS = ['epub', 'fb2', 'pdf', 'djvu', 'txt'] VENDOR_ID = [0x1006] - PRODUCT_ID = [0x4023, 0x4025] + PRODUCT_ID = [0x4023, 0x4024, 0x4025] BCD = [0x0323] VENDOR_NAME = 'IRIVER' - WINDOWS_MAIN_MEM = ['STORY', 'STORY_EB05'] + WINDOWS_MAIN_MEM = ['STORY', 'STORY_EB05', 'STORY_WI-FI'] WINDOWS_CARD_A_MEM = ['STORY', 'STORY_SD'] #OSX_MAIN_MEM = 'Kindle Internal Storage Media' From 2b699deac6e88613332990caa4e5661328ba7e82 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 27 Dec 2010 09:47:41 +0000 Subject: [PATCH 21/58] Fix the folder device to change the USB codes from ints to lists. --- src/calibre/devices/folder_device/driver.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/calibre/devices/folder_device/driver.py b/src/calibre/devices/folder_device/driver.py index d2bcf7ce3d..b852715b97 100644 --- a/src/calibre/devices/folder_device/driver.py +++ b/src/calibre/devices/folder_device/driver.py @@ -18,9 +18,9 @@ class FOLDER_DEVICE_FOR_CONFIG(USBMS): supported_platforms = ['windows', 'osx', 'linux'] FORMATS = ['epub', 'fb2', 'mobi', 'azw', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb', 'prc'] - VENDOR_ID = 0xffff - PRODUCT_ID = 0xffff - BCD = 0xffff + VENDOR_ID = [0xffff] + PRODUCT_ID = [0xffff] + BCD = [0xffff] DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE' @@ -34,9 +34,9 @@ class FOLDER_DEVICE(USBMS): supported_platforms = ['windows', 'osx', 'linux'] FORMATS = FOLDER_DEVICE_FOR_CONFIG.FORMATS - VENDOR_ID = 0xffff - PRODUCT_ID = 0xffff - BCD = 0xffff + VENDOR_ID = [0xffff] + PRODUCT_ID = [0xffff] + BCD = [0xffff] DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE' THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device From 24a4261378a0f5ac7232bcb6e6d6a651491b60dc Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 27 Dec 2010 06:58:39 -0500 Subject: [PATCH 22/58] PDF Output: Add missing option when generating the PDF Printer for comics. --- src/calibre/ebooks/pdf/writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index 2ae1638a73..2b4ba35d3e 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -217,7 +217,7 @@ class ImagePDFWriter(object): os.remove(f.name) def render_images(self, outpath, mi, items): - printer = get_pdf_printer(self.opts) + printer = get_pdf_printer(self.opts, for_comic=True) printer.setOutputFileName(outpath) printer.setDocName(mi.title) printer.setCreator(u'%s [%s]'%(__appname__, __version__)) From b87d3eb24882beb16cf2e33bb736884a8f6aa491 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Dec 2010 10:17:27 -0700 Subject: [PATCH 23/58] Updated Heraldo de Aragon --- resources/recipes/heraldo.recipe | 73 +++++++++++++++++++------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/resources/recipes/heraldo.recipe b/resources/recipes/heraldo.recipe index 381e97b9ce..c5669e116b 100644 --- a/resources/recipes/heraldo.recipe +++ b/resources/recipes/heraldo.recipe @@ -1,50 +1,65 @@ #!/usr/bin/env python -__license__ = 'GPL v3' -__author__ = 'Lorenzo Vigentini' -__copyright__ = '2009, Lorenzo Vigentini ' +__license__ = 'GPL v3' +__copyright__ = '04 December 2010, desUBIKado' +__author__ = 'desUBIKado' __description__ = 'Daily newspaper from Aragon' -__version__ = 'v1.01' -__date__ = '30, January 2010' - +__version__ = 'v0.03' +__date__ = '11, December 2010' ''' -http://www.heraldo.es/ +[url]http://www.heraldo.es/[/url] ''' +import time from calibre.web.feeds.news import BasicNewsRecipe class heraldo(BasicNewsRecipe): - author = 'Lorenzo Vigentini' + __author__ = 'desUBIKado' description = 'Daily newspaper from Aragon' - - cover_url = 'http://www.heraldo.es/MODULOS/global/publico/interfaces/img/logo.gif' title = u'Heraldo de Aragon' publisher = 'OJD Nielsen' category = 'News, politics, culture, economy, general interest' - language = 'es' timefmt = '[%a, %d %b, %Y]' - oldest_article = 1 - max_articles_per_feed = 25 - + max_articles_per_feed = 100 use_embedded_content = False - recursion = 10 - remove_javascript = True no_stylesheets = True - - keep_only_tags = [ - dict(name='div', attrs={'class':['titularNoticiaNN','textoGrisVerdanaContenidos']}) - ] + recursion = 10 feeds = [ - (u'Portadas ', u'http://www.heraldo.es/index.php/mod.portadas/mem.rss') - ] + (u'Portadas', u'http://www.heraldo.es/index.php/mod.portadas/mem.rss') + ] + + + + keep_only_tags = [dict(name='div', attrs={'id':['dts','com']})] + + remove_tags = [dict(name='a', attrs={'class':['com flo-r','enl-if','enl-df']}), + dict(name='div', attrs={'class':['brb-b-s con marg-btt','cnt-rel con']}), + dict(name='form', attrs={'class':'form'})] + + remove_tags_before = dict(name='div' , attrs={'id':'dts'}) + remove_tags_after = dict(name='div' , attrs={'id':'com'}) + + def get_cover_url(self): + cover = None + st = time.localtime() + year = str(st.tm_year) + month = "%.2d" % st.tm_mon + day = "%.2d" % st.tm_mday + #[url]http://oldorigin-www.heraldo.es/20101211/primeras/portada_aragon.pdf[/url] + cover='http://oldorigin-www.heraldo.es/'+ year + month + day +'/primeras/portada_aragon.pdf' + br = BasicNewsRecipe.get_browser() + try: + br.open(cover) + except: + self.log("\nPortada no disponible") + cover ='http://www.heraldo.es/MODULOS/global/publico/interfaces/img/logo-Heraldo.png' + return cover + + + extra_css = ''' - .articledate {color: gray;font-family: monospace;} - .articledescription {display: block;font-family: sans;font-size: 0.7em; text-indent: 0;} - .firma {color: #666;display: block;font-family: verdana, arial, helvetica;font-size: 1em;margin-bottom: 8px;} - .textoGrisVerdanaContenidos {color: #56595c;display: block;font-family: Verdana;font-size: 1.28571em;padding-bottom: 10px} - .titularNoticiaNN {display: block;padding-bottom: 10px;padding-left: 0;padding-right: 0;padding-top: 4px} - .titulo {color: #003066;font-family: Tahoma;font-size: 1.92857em;font-weight: bold;line-height: 1.2em} - ''' + h2{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:xx-large;} + ''' From 9ad49466f74975cd704581b815638b3cdd2ac052 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Dec 2010 10:50:32 -0700 Subject: [PATCH 24/58] El Periodico and Red Aragon by desUBIKado --- resources/recipes/el_periodico.recipe | 109 ++++++++++++++++++++++++++ resources/recipes/red_aragon.recipe | 47 +++++++++++ 2 files changed, 156 insertions(+) create mode 100644 resources/recipes/el_periodico.recipe create mode 100644 resources/recipes/red_aragon.recipe diff --git a/resources/recipes/el_periodico.recipe b/resources/recipes/el_periodico.recipe new file mode 100644 index 0000000000..2c3ed456fb --- /dev/null +++ b/resources/recipes/el_periodico.recipe @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__license__ = 'GPL v3' +__copyright__ = '04 December 2010, desUBIKado' +__author__ = 'desUBIKado' +__description__ = 'Daily newspaper from Aragon' +__version__ = 'v0.05' +__date__ = '07, December 2010' +''' +elperiodicodearagon.com +''' +import re +from calibre.web.feeds.news import BasicNewsRecipe + + +class elperiodicodearagon(BasicNewsRecipe): + title = u'El Periodico de Aragon' + __author__ = u'desUBIKado' + description = u'Noticias desde Aragon' + publisher = u'elperiodicodearagon.com' + category = u'news, politics, Spain, Aragon' + oldest_article = 2 + delay = 0 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + language = 'es' + encoding = 'utf8' + remove_empty_feeds = True + remove_javascript = True + + + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + feeds = [(u'Arag\xf3n', u'http://elperiodicodearagon.com/RSS/2.xml'), + (u'Internacional', u'http://elperiodicodearagon.com/RSS/4.xml'), + (u'Espa\xf1a', u'http://elperiodicodearagon.com/RSS/3.xml'), + (u'Econom\xeda', u'http://elperiodicodearagon.com/RSS/5.xml'), + (u'Deportes', u'http://elperiodicodearagon.com/RSS/7.xml'), + (u'Real Zaragoza', u'http://elperiodicodearagon.com/RSS/10.xml'), + (u'Opini\xf3n', u'http://elperiodicodearagon.com/RSS/103.xml'), + (u'Escenarios', u'http://elperiodicodearagon.com/RSS/105.xml'), + (u'Sociedad', u'http://elperiodicodearagon.com/RSS/104.xml'), + (u'Gente', u'http://elperiodicodearagon.com/RSS/330.xml')] + + + extra_css = ''' + h3{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:xx-large;} + h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} + dd{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} + ''' + + remove_attributes = ['height','width'] + + keep_only_tags = [dict(name='div', attrs={'id':'contenidos'})] + + + # Quitar toda la morralla + + remove_tags = [dict(name='ul', attrs={'class':'herramientasDeNoticia'}), + dict(name='span', attrs={'class':'MasInformacion '}), + dict(name='span', attrs={'class':'MasInformacion'}), + dict(name='div', attrs={'class':'Middle'}), + dict(name='div', attrs={'class':'MenuCabeceraRZaragoza'}), + dict(name='div', attrs={'id':'MenuCabeceraRZaragoza'}), + dict(name='div', attrs={'class':'MenuEquipo'}), + dict(name='div', attrs={'class':'TemasRelacionados'}), + dict(name='div', attrs={'class':'GaleriaEnNoticia'}), + dict(name='div', attrs={'class':'Recorte'}), + dict(name='div', attrs={'id':'NoticiasenRecursos'}), + dict(name='div', attrs={'id':'NoticiaEnPapel'}), + dict(name='p', attrs={'class':'RecorteEnNoticias'}), + dict(name='div', attrs={'id':'Comparte'}), + dict(name='div', attrs={'id':'CajaComparte'}), + dict(name='a', attrs={'class':'EscribirComentario'}), + dict(name='a', attrs={'class':'AvisoComentario'}), + dict(name='div', attrs={'class':'CajaAvisoComentario'}), + dict(name='div', attrs={'class':'navegaNoticias'}), + dict(name='div', attrs={'id':'PaginadorDiCom'}), + dict(name='div', attrs={'id':'CajaAccesoCuentaUsuario'}), + dict(name='div', attrs={'id':'CintilloComentario'}), + dict(name='div', attrs={'id':'EscribeComentario'}), + dict(name='div', attrs={'id':'FormularioComentario'}), + dict(name='div', attrs={'id':'FormularioNormas'})] + + # Recuperamos la portada de papel (la imagen format=1 tiene mayor resolucion) + + def get_cover_url(self): + index = 'http://pdf.elperiodicodearagon.com/' + soup = self.index_to_soup(index) + for image in soup.findAll('img',src=True): + if image['src'].startswith('http://pdf.elperiodicodearagon.com/funciones/portada-preview.php?eid='): + return image['src'].rstrip('format=2') + 'format=1' + return None + + # Para quitar espacios entre la noticia y los comentarios (lineas 1 y 2) + # El indice no apuntaba correctamente al empiece de la noticia (linea 3) + + preprocess_regexps = [ + (re.compile(r'

 

', re.DOTALL|re.IGNORECASE), lambda match: ''), + (re.compile(r'

', re.DOTALL|re.IGNORECASE), lambda match: ''), + (re.compile(r'

', re.DOTALL|re.IGNORECASE), lambda match: '

') + ] diff --git a/resources/recipes/red_aragon.recipe b/resources/recipes/red_aragon.recipe new file mode 100644 index 0000000000..4681e6660b --- /dev/null +++ b/resources/recipes/red_aragon.recipe @@ -0,0 +1,47 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '11 December 2010, desUBIKado' +__author__ = 'desUBIKado' +__description__ = 'Entertainment guide from Aragon' +__version__ = 'v0.01' +__date__ = '11, December 2010' +''' +[url]http://www.redaragon.es/[/url] +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class heraldo(BasicNewsRecipe): + __author__ = 'desUBIKado' + description = u'Guia de ocio desde Aragon' + title = u'RedAragon' + publisher = 'Grupo Z' + category = 'Concerts, Movies, Entertainment news' + cover_url = 'http://www.redaragon.com/2008_img/logotipo.gif' + language = 'es' + timefmt = '[%a, %d %b, %Y]' + oldest_article = 15 + max_articles_per_feed = 100 + encoding = 'iso-8859-1' + use_embedded_content = False + remove_javascript = True + no_stylesheets = True + + feeds = [(u'Conciertos', u'http://redaragon.com/rss/agenda.asp?tid=1'), + (u'Exposiciones', u'http://redaragon.com/rss/agenda.asp?tid=5'), + (u'Teatro', u'http://redaragon.com/rss/agenda.asp?tid=10'), + (u'Conferencias', u'http://redaragon.com/rss/agenda.asp?tid=2'), + (u'Ferias', u'http://redaragon.com/rss/agenda.asp?tid=6'), + (u'Filmotecas/Cineclubs', u'http://redaragon.com/rss/agenda.asp?tid=7'), + (u'Presentaciones', u'http://redaragon.com/rss/agenda.asp?tid=9'), + (u'Fiestas', u'http://redaragon.com/rss/agenda.asp?tid=11'), + (u'Infantil', u'http://redaragon.com/rss/agenda.asp?tid=13'), + (u'Otros', u'http://redaragon.com/rss/agenda.asp?tid=8')] + + keep_only_tags = [dict(name='div', attrs={'id':'FichaEventoAgenda'})] + + remove_tags = [dict(name='div', attrs={'class':['Comparte','CajaAgenda','Caja','Cintillo']})] + + remove_tags_before = dict(name='div' , attrs={'id':'FichaEventoAgenda'}) + + remove_tags_after = dict(name='div' , attrs={'class':'Cintillo'}) From 3d102d6ad8c188bc18621ca2f70cc35920f9f041 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 27 Dec 2010 22:55:10 +0000 Subject: [PATCH 25/58] Fix #8080 - Sort in library doesn't work after bulk metadata edit. In fact it has nothing to do with metadata edit, but instead comes from mixing int and bool flags for ascending and descending. --- src/calibre/gui2/library/models.py | 2 +- src/calibre/gui2/library/views.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 920753a77d..22a9db0fef 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -252,7 +252,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.db.sort(label, ascending) if reset: self.reset() - self.sorted_on = (label, order) + self.sorted_on = (label, order == Qt.AscendingOrder) self.sort_history.insert(0, self.sorted_on) self.sorting_done.emit(self.db.index) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 8dad4c21b1..457cfaf754 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -284,15 +284,19 @@ class BooksView(QTableView): # {{{ for col, order in sort_history: if col == 'date': col = 'timestamp' - if col in self.column_map and (not history or history[0][0] != col): - history.append([col, order]) + if col in self.column_map: + if (not history or history[0][0] != col): + history.append([col, order]) + elif isinstance(order, bool) and history[0][1] != order: + history[0][1] = order return history def apply_sort_history(self, saved_history): if not saved_history: return for col, order in reversed(self.cleanup_sort_history(saved_history)[:3]): - self.sortByColumn(self.column_map.index(col), order) + self.sortByColumn(self.column_map.index(col), + Qt.AscendingOrder if order else Qt.DescendingOrder) def apply_state(self, state): h = self.column_header From 9a16081b0caffc11b62d9b10af2985e73ac8faec Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Wed, 29 Dec 2010 03:14:59 +0800 Subject: [PATCH 26/58] [Device] Add non-USB (Wi-Fi) connection support for Bambook. --- src/calibre/devices/bambook/driver.py | 24 +++++++++++++++++-- src/calibre/devices/bambook/libbambookcore.py | 2 ++ src/calibre/gui2/actions/device.py | 18 ++++++++++++++ src/calibre/gui2/device.py | 5 ++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/bambook/driver.py b/src/calibre/devices/bambook/driver.py index 930c67a159..94a19df998 100644 --- a/src/calibre/devices/bambook/driver.py +++ b/src/calibre/devices/bambook/driver.py @@ -29,12 +29,15 @@ class BAMBOOK(DeviceConfig, DevicePlugin): booklist_class = BookList book_class = Book + ip = None + FORMATS = [ "snb" ] VENDOR_ID = 0x230b PRODUCT_ID = 0x0001 BCD = None CAN_SET_METADATA = False THUMBNAIL_HEIGHT = 155 + EXTRA_CUSTOMIZATION_MESSAGE = _("Device IP Address") icon = I("devices/bambook.png") # OPEN_FEEDBACK_MESSAGE = _( @@ -47,6 +50,10 @@ class BAMBOOK(DeviceConfig, DevicePlugin): METADATA_FILE_GUID = 'calibremetadata.snb' bambook = None + is_connected = False + + def __init__(self, ip): + self.ip = ip def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None) : @@ -60,15 +67,23 @@ class BAMBOOK(DeviceConfig, DevicePlugin): self.eject() # Connect self.bambook = Bambook() - self.bambook.Connect() + self.bambook.Connect(ip = self.ip, timeout = 10000) if self.bambook.GetState() != CONN_CONNECTED: self.bambook = None - raise Exception(_("Unable to connect to Bambook.")) + raise OpenFeedback(_("Unable to connect to Bambook. \n" + "If you are trying to connect via Wi-Fi, " + "please make sure the IP address of Bambook has been correctly configured.")) + self.is_connected = True + return True + + def unmount_device(self): + self.eject() def eject(self): if self.bambook: self.bambook.Disconnect() self.bambook = None + self.is_connected = False def post_yank_cleanup(self): self.eject() @@ -475,3 +490,8 @@ class BAMBOOK(DeviceConfig, DevicePlugin): def get_guid(uuid): guid = hashlib.md5(uuid).hexdigest()[0:15] + ".snb" return guid + +class BAMBOOKWifi(BAMBOOK): + def is_usb_connected(self, devices_on_system, debug=False, + only_presence=False): + return self.is_connected, self diff --git a/src/calibre/devices/bambook/libbambookcore.py b/src/calibre/devices/bambook/libbambookcore.py index a11c5e9e87..35d04ba4ac 100644 --- a/src/calibre/devices/bambook/libbambookcore.py +++ b/src/calibre/devices/bambook/libbambookcore.py @@ -329,6 +329,8 @@ class Bambook: self.handle = None def Connect(self, ip = DEFAULT_BAMBOOK_IP, timeout = 10000): + if ip == None or ip == '': + ip = DEFAULT_BAMBOOK_IP self.handle = BambookConnect(ip, timeout) if self.handle and self.handle != 0: return True diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 744ab20d10..35bfd2bf6a 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -12,11 +12,15 @@ from PyQt4.Qt import QToolButton, QMenu, pyqtSignal, QIcon from calibre.gui2.actions import InterfaceAction from calibre.utils.smtp import config as email_config from calibre.constants import iswindows, isosx +from calibre.customize.ui import is_disabled +from calibre.devices.bambook.driver import BAMBOOK class ShareConnMenu(QMenu): # {{{ connect_to_folder = pyqtSignal() connect_to_itunes = pyqtSignal() + connect_to_bambook = pyqtSignal() + config_email = pyqtSignal() toggle_server = pyqtSignal() dont_add_to = frozenset(['toolbar-device', 'context-menu-device']) @@ -34,6 +38,12 @@ class ShareConnMenu(QMenu): # {{{ self.connect_to_itunes_action = mitem if not (iswindows or isosx): mitem.setVisible(False) + mitem = self.addAction(QIcon(I('devices/bambook.png')), _('Connect to Bambook')) + mitem.setEnabled(True) + mitem.triggered.connect(lambda x : self.connect_to_bambook.emit()) + self.connect_to_bambook_action = mitem + if is_disabled(BAMBOOK): + mitem.setVisible(False) self.addSeparator() self.toggle_server_action = \ self.addAction(QIcon(I('network-server.png')), @@ -88,6 +98,13 @@ class ShareConnMenu(QMenu): # {{{ def set_state(self, device_connected): self.connect_to_folder_action.setEnabled(not device_connected) self.connect_to_itunes_action.setEnabled(not device_connected) + self.connect_to_bambook_action.setEnabled(not device_connected) + bambook_visible = False + if not is_disabled(BAMBOOK): + device_ip = BAMBOOK.settings().extra_customization + if device_ip != None and device_ip != '': + bambook_visible = True + self.connect_to_bambook_action.setVisible(bambook_visible) # }}} @@ -126,6 +143,7 @@ class ConnectShareAction(InterfaceAction): self.qaction.setMenu(self.share_conn_menu) self.share_conn_menu.connect_to_folder.connect(self.gui.connect_to_folder) self.share_conn_menu.connect_to_itunes.connect(self.gui.connect_to_itunes) + self.share_conn_menu.connect_to_bambook.connect(self.gui.connect_to_bambook) def location_selected(self, loc): enabled = loc == 'library' diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 3b071aa024..6d289a3e5c 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -24,6 +24,7 @@ from calibre.utils.filenames import ascii_filename 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 @@ -635,6 +636,10 @@ class DeviceMixin(object): # {{{ if dir is not None: self.device_manager.mount_device(kls=FOLDER_DEVICE, kind='folder', path=dir) + def connect_to_bambook(self): + self.device_manager.mount_device(kls=BAMBOOKWifi, kind='bambook', + path=BAMBOOK.settings().extra_customization) + def connect_to_itunes(self): self.device_manager.mount_device(kls=ITUNES_ASYNC, kind='itunes', path=None) From 6f718a4dc9e12ee355e05374632d51f4ba880a97 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 29 Dec 2010 08:38:58 +0000 Subject: [PATCH 27/58] Fix fetching of custom series indices using book.get(...) --- src/calibre/ebooks/metadata/book/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 22752ca09e..e3fb8092e6 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -159,6 +159,11 @@ class Metadata(object): try: return self.__getattribute__(field) except AttributeError: + if field.startswith('#') and field.endswith('_index'): + try: + return self.get_extra(field[:-6]) + except: + pass return default def get_extra(self, field): From 62b4676cb8548fce020e22d5822673f6ef2ba395 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 29 Dec 2010 13:05:18 +0000 Subject: [PATCH 28/58] Subcategories in tag browser --- resources/default_tweaks.py | 26 +++++++ src/calibre/gui2/tag_view.py | 129 ++++++++++++++++++++++++++--------- 2 files changed, 122 insertions(+), 33 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index efa46fa7ae..a2a9a0a043 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -55,6 +55,32 @@ author_sort_copy_method = 'invert' # categories_use_field_for_author_name = 'author_sort' categories_use_field_for_author_name = 'author' +# Control how the tags pane displays categories containing many items. If the +# number of items is larger than categories_collapse_more_than, a sub-category +# will be added. If sorting by name, then the subcategories can be organized by +# first letter (categories_collapse_model = 'first letter') or into equal-sized +# groups (categories_collapse_model = 'partition'). If sorting by average rating +# or by popularity, then 'partition' is always used. The addition of +# subcategories can be disabled by setting categories_collapse_more_than = 0. +# When using partition, the format of the subcategory label is controlled by a +# template: categories_collapsed_name_template if sorting by name, +# categories_collapsed_rating_template if sorting by average rating, and +# categories_collapsed_popularity_template if sorting by popularity. There are +# two variables available to the template: first and last. The variable 'first' +# is the initial item in the subcategory, and the variable 'last' is the final +# item in the subcategory. Both variables are 'objects'; they each have multiple +# values that are obtained by using a suffix. For example, first.name for an +# author category will be the name of the author. The sub-values available are: +# name: the printable name of the item +# count: the number of books that references this item +# avg_rating: the averate rating of all the books referencing this item +# sort: the sort value. For authors, this is the author_sort for that author +# category: the category (e.g., authors, series) that the item is in. +categories_collapse_more_than = 50 +categories_collapsed_name_template = '{first.name:shorten(4,'',0)}{last.name::shorten(4,'',0)| - |}' +categories_collapsed_rating_template = '{first.avg_rating:4.2f}{last.avg_rating:4.2f| - |}' +categories_collapsed_popularity_template = '{first.count:d}{last.count:d| - |}' +categories_collapse_model = 'first letter' # Set whether boolean custom columns are two- or three-valued. # Two-values for true booleans diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 3d43d49a75..345ee50031 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -18,9 +18,11 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \ from calibre.ebooks.metadata import title_sort from calibre.gui2 import config, NONE from calibre.library.field_metadata import TagsIcons, category_icon_map +from calibre.library.database2 import Tag from calibre.utils.config import tweaks -from calibre.utils.icu import sort_key +from calibre.utils.icu import sort_key, upper from calibre.utils.search_query_parser import saved_searches +from calibre.utils.formatter import eval_formatter from calibre.gui2 import error_dialog from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.tag_categories import TagCategories @@ -400,7 +402,7 @@ class TagTreeItem(object): # {{{ def category_data(self, role): if role == Qt.DisplayRole: - return QVariant(self.py_name + ' [%d]'%len(self.children)) + return QVariant(self.py_name + ' [%d]'%len(self.child_tags())) if role == Qt.DecorationRole: return self.icon if role == Qt.FontRole: @@ -441,6 +443,15 @@ class TagTreeItem(object): # {{{ if self.type == self.TAG: self.tag.state = (self.tag.state + 1)%3 + def child_tags(self): + res = [] + for t in self.children: + if t.type == TagTreeItem.CATEGORY: + for c in t.children: + res.append(c) + else: + res.append(t) + return res # }}} class TagsModel(QAbstractItemModel): # {{{ @@ -477,19 +488,11 @@ class TagsModel(QAbstractItemModel): # {{{ tt = _('The lookup/search name is "{0}"').format(r) else: tt = '' - c = TagTreeItem(parent=self.root_item, + TagTreeItem(parent=self.root_item, data=self.categories[i], category_icon=self.category_icon_map[r], tooltip=tt, category_key=r) - # This duplicates code in refresh(). Having it here as well - # can save seconds during startup, because we avoid a second - # call to get_node_tree. - for tag in data[r]: - if r not in self.categories_with_ratings and \ - not self.db.field_metadata[r]['is_custom'] and \ - not self.db.field_metadata[r]['kind'] == 'user': - tag.avg_rating = None - TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) + self.refresh(data=data) def mimeTypes(self): return ["application/calibre+from_library"] @@ -652,35 +655,85 @@ class TagsModel(QAbstractItemModel): # {{{ return None return data - def refresh(self): - data = self.get_node_tree(config['sort_tags_by']) # get category data + def refresh(self, data=None): + sort_by = config['sort_tags_by'] + if data is None: + data = self.get_node_tree(sort_by) # get category data if data is None: return False row_index = -1 + empty_tag = Tag('') + collapse = tweaks['categories_collapse_more_than'] + collapse_model = tweaks['categories_collapse_model'] + if sort_by == 'name': + collapse_template = tweaks['categories_collapsed_name_template'] + elif sort_by == 'rating': + collapse_model = 'partition' + collapse_template = tweaks['categories_collapsed_rating_template'] + else: + collapse_model = 'partition' + collapse_template = tweaks['categories_collapsed_popularity_template'] + collapse_letter = None + for i, r in enumerate(self.row_map): if self.hidden_categories and self.categories[i] in self.hidden_categories: continue row_index += 1 category = self.root_item.children[row_index] - names = [t.tag.name for t in category.children] - states = [t.tag.state for t in category.children] + names = [] + states = [] + children = category.child_tags() + states = [t.tag.state for t in children] + names = [t.tag.name for names in children] state_map = dict(izip(names, states)) category_index = self.index(row_index, 0, QModelIndex()) + category_node = category_index.internalPointer() if len(category.children) > 0: self.beginRemoveRows(category_index, 0, len(category.children)-1) category.children = [] self.endRemoveRows() - if len(data[r]) > 0: - self.beginInsertRows(category_index, 0, len(data[r])-1) - for tag in data[r]: - if r not in self.categories_with_ratings and \ + cat_len = len(data[r]) + if cat_len <= 0: + continue + + self.beginInsertRows(category_index, 0, len(data[r])-1) + clear_rating = True if r not in self.categories_with_ratings and \ not self.db.field_metadata[r]['is_custom'] and \ - not self.db.field_metadata[r]['kind'] == 'user': - tag.avg_rating = None - tag.state = state_map.get(tag.name, 0) + not self.db.field_metadata[r]['kind'] == 'user' \ + else False + for idx,tag in enumerate(data[r]): + if clear_rating: + tag.avg_rating = None + tag.state = state_map.get(tag.name, 0) + + if collapse > 0 and cat_len > collapse: + if collapse_model == 'partition': + if (idx % collapse) == 0: + d = {'first': tag} + if cat_len > idx + collapse: + d['last'] = data[r][idx+collapse-1] + else: + d['last'] = empty_tag + name = eval_formatter.safe_format(collapse_template, + d, 'TAG_VIEW', None) + sub_cat = TagTreeItem(parent=category, + data = name, tooltip = None, + category_icon = category_node.icon, + category_key=category_node.category_key) + else: + if upper(tag.name[0]) != collapse_letter: + collapse_letter = upper(tag.name[0]) + sub_cat = TagTreeItem(parent=category, + data = collapse_letter, + category_icon = category_node.icon, + tooltip = None, + category_key=category_node.category_key) + t = TagTreeItem(parent=sub_cat, data=tag, + icon_map=self.icon_state_map) + else: t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map) - self.endInsertRows() + self.endInsertRows() return True def columnCount(self, parent): @@ -824,19 +877,28 @@ class TagsModel(QAbstractItemModel): # {{{ def reset_all_states(self, except_=None): update_list = [] + def process_tag(tag_item): + tag = tag_item.tag + if tag is except_: + self.dataChanged.emit(tag_index, tag_index) + return + if tag.state != 0 or tag in update_list: + tag.state = 0 + update_list.append(tag) + self.dataChanged.emit(tag_index, tag_index) + for i in xrange(self.rowCount(QModelIndex())): category_index = self.index(i, 0, QModelIndex()) for j in xrange(self.rowCount(category_index)): tag_index = self.index(j, 0, category_index) tag_item = tag_index.internalPointer() - tag = tag_item.tag - if tag is except_: - self.dataChanged.emit(tag_index, tag_index) - continue - if tag.state != 0 or tag in update_list: - tag.state = 0 - update_list.append(tag) - self.dataChanged.emit(tag_index, tag_index) + if tag_item.type == TagTreeItem.CATEGORY: + for k in xrange(self.rowCount(tag_index)): + ti = self.index(k, 0, tag_index) + ti = ti.internalPointer() + process_tag(ti) + else: + process_tag(tag_item) def clear_state(self): self.reset_all_states() @@ -856,6 +918,7 @@ class TagsModel(QAbstractItemModel): # {{{ ans = [] tags_seen = set() row_index = -1 + for i, key in enumerate(self.row_map): if self.hidden_categories and self.categories[i] in self.hidden_categories: continue @@ -863,7 +926,7 @@ class TagsModel(QAbstractItemModel): # {{{ if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category continue category_item = self.root_item.children[row_index] - for tag_item in category_item.children: + for tag_item in category_item.child_tags(): tag = tag_item.tag if tag.state > 0: prefix = ' not ' if tag.state == 2 else '' From 773ee07343dfb9c904438723f79e287db9f8529c Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Wed, 29 Dec 2010 22:36:31 +0800 Subject: [PATCH 29/58] [Bug] Better error handling if some meta data is missing in the SNB file. --- src/calibre/ebooks/snb/input.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/calibre/ebooks/snb/input.py b/src/calibre/ebooks/snb/input.py index 659ca79619..d2acb257aa 100755 --- a/src/calibre/ebooks/snb/input.py +++ b/src/calibre/ebooks/snb/input.py @@ -46,14 +46,27 @@ class SNBInput(InputFormatPlugin): meta = snbFile.GetFileStream('snbf/book.snbf') if meta != None: meta = etree.fromstring(meta) - oeb.metadata.add('title', meta.find('.//head/name').text) - oeb.metadata.add('creator', meta.find('.//head/author').text, attrib={'role':'aut'}) - oeb.metadata.add('language', meta.find('.//head/language').text.lower().replace('_', '-')) - oeb.metadata.add('creator', meta.find('.//head/generator').text) - oeb.metadata.add('publisher', meta.find('.//head/publisher').text) - cover = meta.find('.//head/cover') - if cover != None and cover.text != None: - oeb.guide.add('cover', 'Cover', cover.text) + l = { 'title' : './/head/name', + 'creator' : './/head/author', + 'language' : './/head/language', + 'generator': './/head/generator', + 'publisher': './/head/publisher', + 'cover' : './/head/cover', } + d = {} + for item in l: + node = meta.find(l[item]) + if node != None: + d[item] = node.text if node.text != None else '' + else: + d[item] = '' + + oeb.metadata.add('title', d['title']) + oeb.metadata.add('creator', d['creator'], attrib={'role':'aut'}) + oeb.metadata.add('language', d['language'].lower().replace('_', '-')) + oeb.metadata.add('generator', d['generator']) + oeb.metadata.add('publisher', d['publisher']) + if d['cover'] != '': + oeb.guide.add('cover', 'Cover', d['cover']) bookid = str(uuid.uuid4()) oeb.metadata.add('identifier', bookid, id='uuid_id', scheme='uuid') From a79d75bd5a36c35a67e38d6350b487e51afdf88a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 29 Dec 2010 16:17:56 +0000 Subject: [PATCH 30/58] Eliminate the last 3-level depth code --- src/calibre/gui2/tag_view.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 345ee50031..7d3b82e00c 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -877,7 +877,7 @@ class TagsModel(QAbstractItemModel): # {{{ def reset_all_states(self, except_=None): update_list = [] - def process_tag(tag_item): + def process_tag(tag_index, tag_item): tag = tag_item.tag if tag is except_: self.dataChanged.emit(tag_index, tag_index) @@ -887,18 +887,17 @@ class TagsModel(QAbstractItemModel): # {{{ update_list.append(tag) self.dataChanged.emit(tag_index, tag_index) - for i in xrange(self.rowCount(QModelIndex())): - category_index = self.index(i, 0, QModelIndex()) + def process_level(category_index): for j in xrange(self.rowCount(category_index)): tag_index = self.index(j, 0, category_index) tag_item = tag_index.internalPointer() if tag_item.type == TagTreeItem.CATEGORY: - for k in xrange(self.rowCount(tag_index)): - ti = self.index(k, 0, tag_index) - ti = ti.internalPointer() - process_tag(ti) + process_level(tag_index) else: - process_tag(tag_item) + process_tag(tag_index, tag_item) + + for i in xrange(self.rowCount(QModelIndex())): + process_level(self.index(i, 0, QModelIndex())) def clear_state(self): self.reset_all_states() From e24150ade3b84f72e72ce009bae9a00aef7662d2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 29 Dec 2010 11:17:04 -0700 Subject: [PATCH 31/58] Fix #8102 (Updated recipe for Wired Magazine) --- resources/recipes/wired.recipe | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/resources/recipes/wired.recipe b/resources/recipes/wired.recipe index 9599d54de9..bb9a97f5c4 100644 --- a/resources/recipes/wired.recipe +++ b/resources/recipes/wired.recipe @@ -38,12 +38,12 @@ class Wired(BasicNewsRecipe): keep_only_tags = [dict(name='div', attrs={'class':'post'})] remove_tags_after = dict(name='div', attrs={'class':'tweetmeme_button'}) remove_tags = [ - dict(name=['object','embed','iframe','link']) + dict(name=['object','embed','iframe','link','meta','base']) ,dict(name='div', attrs={'class':['podcast_storyboard','tweetmeme_button']}) ,dict(attrs={'id':'ff_bottom_nav'}) ,dict(name='a',attrs={'href':'http://www.wired.com/app'}) ] - remove_attributes = ['height','width'] + remove_attributes = ['height','width','lang','border','clear'] def parse_index(self): @@ -78,7 +78,9 @@ class Wired(BasicNewsRecipe): divurl = item.find('div',attrs={'class':'feature-header'}) if divurl: divdesc = item.find('div',attrs={'class':'feature-text'}) - url = 'http://www.wired.com' + divurl.a['href'] + url = divurl.a['href'] + if not divurl.a['href'].startswith('http://www.wired.com'): + url = 'http://www.wired.com' + divurl.a['href'] title = self.tag_to_string(divurl.a) description = self.tag_to_string(divdesc) date = strftime(self.timefmt) @@ -127,5 +129,17 @@ class Wired(BasicNewsRecipe): def preprocess_html(self, soup): for item in soup.findAll(style=True): del item['style'] + for item in soup.findAll('a'): + if item.string is not None: + tstr = item.string + item.replaceWith(tstr) + else: + item.name='span' + for atrs in ['href','target','alt','title','name','id']: + if item.has_key(atrs): + del item[atrs] + for item in soup.findAll('img'): + if not item.has_key('alt'): + item['alt'] = 'image' return soup From 14fa34a4fb0dfe25a753030adda038f9c5640c63 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 29 Dec 2010 11:26:35 -0700 Subject: [PATCH 32/58] Business Insider by DM. Fixes #8087 (New recipe for Business insider) --- resources/images/news/business_insider.png | Bin 0 -> 1147 bytes resources/recipes/business_insider.recipe | 69 +++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 resources/images/news/business_insider.png create mode 100644 resources/recipes/business_insider.recipe diff --git a/resources/images/news/business_insider.png b/resources/images/news/business_insider.png new file mode 100644 index 0000000000000000000000000000000000000000..7e86e583e8f7b1d127561bfdce0b19d24536f15e GIT binary patch literal 1147 zcmV->1cdvEP)jx@mQ;UIZB-$VrwSp~(7DVynMSAGLi%5^2>fhkO zn+NfzAS#L#6)b29T2Vyf7k;$$gIJTaU2AIdu}Q|mIL>Yw5uMB1$ISfR@4feXvt;+~ zfq}t6Aw19XJP-f02nqT^{QZtPW15UH2C!4_VY-wp} z0e0-zu%WdTIB=k+XXj2J91ev-As~~<<)){BNF)?8O(36l-MKm7`t`ea@81VLeVUre zW_6%MA{J|J2lnmj?(XUW97p2j^RD~t8*t-BDm69+q|@)-jgJG=bqj_0c_5W~@+6%G zmM^z$+Xgy29VZq8?%jL*_~}#N^5r{sQYoOfciXmP5;%40#EIj_0n3U+!eM|ALI?p~ zy_%T#_z}qEW@lX&=k47~CWvkU%tya^eu2dwhil;2o7ggV+ zuY0L{X{V<0`I#A@T$Y;2(0f3*8TE$|yTrLCW&Rx59`!+B+naOYt{f~&Rn`QJPg!o^?JSDtd+{DO3Q>7iEwcrgW^1U_UhGJw}5BQ-oBlf0Q&p) z@87dWk6UAg%_`tenEw8tlG&d`sF;^Zl}e=oJb3W@`Rmt!VHk#C$g$nHQJ-rr!5|(X z0HZX>sQH!Pz7Rm6@Z-nAf z7&8pOwwEliEMWWg&70$K;Ne60u%y%P-%n2J0GxBqm?og`{P8FS80Qi=nRJ|ZT#uc4 zA&(sC>+9_WdV02Q-LeHRO$q$@b2d9Y4P3l9GBP?UMeBKh Date: Wed, 29 Dec 2010 11:35:48 -0700 Subject: [PATCH 33/58] Fix #8083 (HTC Desire + DeFrost ROM not recognized by Android driver.) --- src/calibre/devices/android/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 492b00617d..ced6a45da4 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -64,7 +64,7 @@ class ANDROID(USBMS): WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', - 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000'] + 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD'] From 85bf7da1fb9fc6d695f7e89d2d82d96fa484440e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 29 Dec 2010 11:49:05 -0700 Subject: [PATCH 34/58] Fix #8054 (Hyperlinks in book descriptions don't work) --- src/calibre/gui2/book_details.py | 10 ++++++++-- src/calibre/gui2/dialogs/book_info.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index dd12080d7f..8e3e8b10de 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en' import os, collections, sys from Queue import Queue -from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, \ +from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \ QPropertyAnimation, QEasingCurve, QThread, QApplication, QFontInfo, \ QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette from PyQt4.QtWebKit import QWebView @@ -18,7 +18,7 @@ from calibre.gui2.widgets import IMAGE_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS from calibre.constants import preferred_encoding from calibre.library.comments import comments_to_html -from calibre.gui2 import config, open_local_file +from calibre.gui2 import config, open_local_file, open_url from calibre.utils.icu import sort_key # render_rows(data) {{{ @@ -412,6 +412,12 @@ class BookDetails(QWidget): # {{{ self.view_specific_format.emit(int(id_), fmt) elif typ == 'devpath': open_local_file(val) + else: + try: + open_url(QUrl(link, QUrl.TolerantMode)) + except: + import traceback + traceback.print_exc() def mouseDoubleClickEvent(self, ev): diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 1384c27b8c..eac8461299 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -9,7 +9,7 @@ from PyQt4.Qt import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, \ QDialog, QPixmap, QGraphicsScene, QIcon, QSize from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo -from calibre.gui2 import dynamic, open_local_file +from calibre.gui2 import dynamic, open_local_file, open_url from calibre import fit_image from calibre.library.comments import comments_to_html from calibre.utils.icu import sort_key @@ -22,6 +22,8 @@ class BookInfo(QDialog, Ui_BookInfo): self.setupUi(self) self.cover_pixmap = None self.comments.sizeHint = self.comments_size_hint + self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks) + self.comments.linkClicked(self.link_clicked) self.view_func = view_func @@ -41,6 +43,8 @@ class BookInfo(QDialog, Ui_BookInfo): screen_height = desktop.availableGeometry().height() - 100 self.resize(self.size().width(), screen_height) + def link_clicked(self, url): + open_url(url) def comments_size_hint(self): return QSize(350, 250) @@ -115,6 +119,7 @@ class BookInfo(QDialog, Ui_BookInfo): lines = [x if x.strip() else '

' for x in lines] comments = '\n'.join(lines) self.comments.setHtml('

%s
' % comments) + self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks) cdata = info.pop('cover', '') self.cover_pixmap = QPixmap.fromImage(cdata) self.resize_cover() From a323051dcdf436c7eb87b41fec06a1b9e3a23c20 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 29 Dec 2010 11:51:59 -0700 Subject: [PATCH 35/58] (Imroved El Pais) Fix #8095 (Completely new recipe for El Pais) --- resources/recipes/elpais_impreso.recipe | 141 +++++++++++++----------- 1 file changed, 75 insertions(+), 66 deletions(-) diff --git a/resources/recipes/elpais_impreso.recipe b/resources/recipes/elpais_impreso.recipe index bba3bda217..130013286c 100644 --- a/resources/recipes/elpais_impreso.recipe +++ b/resources/recipes/elpais_impreso.recipe @@ -1,86 +1,95 @@ -# -*- coding: utf-8 -*- __license__ = 'GPL v3' __copyright__ = '2010, Darko Miletic ' ''' -www.elpais.com/diario/ +www.elpais.com ''' -from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe -class ElPaisImpresa(BasicNewsRecipe): - title = u'El Pa\xeds - edicion impresa' +class ElPais_RSS(BasicNewsRecipe): + title = 'El Pais' __author__ = 'Darko Miletic' - description = u'el periodico global en Espa\xf1ol' + description = 'el periodico global en Castellano' publisher = 'EDICIONES EL PAIS, S.L.' - category = 'news, politics,Spain,actualidad,noticias,informacion,videos,fotografias,audios,graficos,nacional,internacional,deportes,economia,tecnologia,cultura,gente,television,sociedad,opinion,blogs,foros,chats,encuestas,entrevistas,participacion' + category = 'news, politics, finances, world, spain' + oldest_article = 2 + max_articles_per_feed = 200 no_stylesheets = True - encoding = 'latin1' + encoding = 'cp1252' use_embedded_content = False - language = 'es' + language = 'es_ES' + remove_empty_feeds = True publication_type = 'newspaper' - masthead_url = 'http://www.elpais.com/im/tit_logo_global.gif' - index = 'http://www.elpais.com/diario/' - extra_css = ' p{text-align: justify} body{ text-align: left; font-family: Georgia,"Times New Roman",Times,serif } h2{font-family: Arial,Helvetica,sans-serif} img{margin-bottom: 0.4em} ' + masthead_url = 'http://www.elpais.com/im/tit_logo.gif' + extra_css = """ + body{font-family: Georgia,"Times New Roman",Times,serif } + h3{font-family: Arial,Helvetica,sans-serif} + img{margin-bottom: 0.4em; display:block} + """ conversion_options = { - 'comment' : description - , 'tags' : category - , 'publisher' : publisher - , 'language' : language + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language } - feeds = [ - (u'Internacional' , index + u'internacional/' ) - ,(u'Espa\xf1a' , index + u'espana/' ) - ,(u'Economia' , index + u'economia/' ) - ,(u'Opinion' , index + u'opinion/' ) - ,(u'Vi\xf1etas' , index + u'vineta/' ) - ,(u'Sociedad' , index + u'sociedad/' ) - ,(u'Cultura' , index + u'cultura/' ) - ,(u'Tendencias' , index + u'tendencias/' ) - ,(u'Gente' , index + u'gente/' ) - ,(u'Obituarios' , index + u'obituarios/' ) - ,(u'Deportes' , index + u'deportes/' ) - ,(u'Pantallas' , index + u'radioytv/' ) - ,(u'Ultima' , index + u'ultima/' ) - ,(u'Educacion' , index + u'educacion/' ) - ,(u'Saludo' , index + u'salud/' ) - ,(u'Ciberpais' , index + u'ciberpais/' ) - ,(u'EP3' , index + u'ep3/' ) - ,(u'Cine' , index + u'cine/' ) - ,(u'Babelia' , index + u'babelia/' ) - ,(u'El viajero' , index + u'viajero/' ) - ,(u'Negocios' , index + u'negocios/' ) - ,(u'Domingo' , index + u'domingo/' ) - ,(u'El Pais semanal' , index + u'eps/' ) - ,(u'Quadern Catalunya' , index + u'quadern-catalunya/' ) - ] + keep_only_tags = [dict(attrs={'class':['cabecera_noticia estirar','cabecera_noticia','','contenido_noticia']})] + remove_tags = [ + dict(name=['meta','link','base','iframe','embed','object']) + ,dict(attrs={'class':['info_complementa','estructura_2col_der','votos estirar','votos']}) + ,dict(attrs={'id':'utilidades'}) + ] + remove_tags_after = dict(attrs={'id':'utilidades'}) + remove_attributes = ['lang','border','width','height'] - keep_only_tags=[dict(attrs={'class':['cabecera_noticia','contenido_noticia']})] - remove_attributes=['width','height'] - remove_tags=[dict(name='link')] - - def parse_index(self): - totalfeeds = [] - lfeeds = self.get_feeds() - for feedobj in lfeeds: - feedtitle, feedurl = feedobj - self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl)) - articles = [] - soup = self.index_to_soup(feedurl) - for item in soup.findAll('a',attrs={'class':['g19r003','g19i003','g17r003','g17i003']}): - url = 'http://www.elpais.com' + item['href'].rpartition('/')[0] - title = self.tag_to_string(item) - date = strftime(self.timefmt) - articles.append({ - 'title' :title - ,'date' :date - ,'url' :url - ,'description':'' - }) - totalfeeds.append((feedtitle, articles)) - return totalfeeds + feeds = [ + (u'Lo ultimo' , u'http://www.elpais.com/rss/feed.html?feedId=17046') + ,(u'America Latina' , u'http://www.elpais.com/rss/feed.html?feedId=17041') + ,(u'Mexico' , u'http://www.elpais.com/rss/feed.html?feedId=17042') + ,(u'Europa' , u'http://www.elpais.com/rss/feed.html?feedId=17043') + ,(u'Estados Unidos' , u'http://www.elpais.com/rss/feed.html?feedId=17044') + ,(u'Oriente proximo' , u'http://www.elpais.com/rss/feed.html?feedId=17045') + ,(u'Espana' , u'http://www.elpais.com/rss/feed.html?feedId=1002' ) + ,(u'Andalucia' , u'http://www.elpais.com/rss/feed.html?feedId=17057') + ,(u'Catalunia' , u'http://www.elpais.com/rss/feed.html?feedId=17059') + ,(u'Comunidad Valenciana' , u'http://www.elpais.com/rss/feed.html?feedId=17061') + ,(u'Madrid' , u'http://www.elpais.com/rss/feed.html?feedId=1016' ) + ,(u'Pais Vasco' , u'http://www.elpais.com/rss/feed.html?feedId=17062') + ,(u'Galicia' , u'http://www.elpais.com/rss/feed.html?feedId=17063') + ,(u'Opinion' , u'http://www.elpais.com/rss/feed.html?feedId=1003' ) + ,(u'Sociedad' , u'http://www.elpais.com/rss/feed.html?feedId=1004' ) + ,(u'Deportes' , u'http://www.elpais.com/rss/feed.html?feedId=1007' ) + ,(u'Cultura' , u'http://www.elpais.com/rss/feed.html?feedId=1008' ) + ,(u'Cine' , u'http://www.elpais.com/rss/feed.html?feedId=17052') + ,(u'Literatura' , u'http://www.elpais.com/rss/feed.html?feedId=17053') + ,(u'Musica' , u'http://www.elpais.com/rss/feed.html?feedId=17051') + ,(u'Arte' , u'http://www.elpais.com/rss/feed.html?feedId=17060') + ,(u'Tecnologia' , u'http://www.elpais.com/rss/feed.html?feedId=1005' ) + ,(u'Economia' , u'http://www.elpais.com/rss/feed.html?feedId=1006' ) + ,(u'Ciencia' , u'http://www.elpais.com/rss/feed.html?feedId=17068') + ,(u'Salud' , u'http://www.elpais.com/rss/feed.html?feedId=17074') + ,(u'Ocio' , u'http://www.elpais.com/rss/feed.html?feedId=17075') + ,(u'Justicia y Leyes' , u'http://www.elpais.com/rss/feed.html?feedId=17069') + ,(u'Guerras y conflictos' , u'http://www.elpais.com/rss/feed.html?feedId=17070') + ,(u'Politica' , u'http://www.elpais.com/rss/feed.html?feedId=17073') + ] def print_version(self, url): return url + '?print=1' + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + for item in soup.findAll('a'): + if item.string is not None: + tstr = item.string + item.replaceWith(tstr) + else: + item.name='span' + for atrs in ['href','target','alt','title']: + if item.has_key(atrs): + del item[atrs] + for item in soup.findAll('img',alt=False): + item['alt'] = 'image' + return soup From 459529d11bc3e1cb271ada1b83dc46c710f31582 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 29 Dec 2010 12:00:04 -0700 Subject: [PATCH 36/58] comments editor: Pass through Esc key --- src/calibre/gui2/comments_editor.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 97a218a10b..1a6b60284b 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -259,6 +259,19 @@ class EditorWidget(QWebView): # {{{ return property(fget=fget, fset=fset) + def keyPressEvent(self, ev): + if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): + ev.ignore() + else: + return QWebView.keyPressed(self, ev) + + def keyReleaseEvent(self, ev): + if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): + ev.ignore() + else: + return QWebView.keyReleased(self, ev) + + # }}} # Highlighter {{{ From 7cdfdb27c25ae3401637df31017fc907a285d84d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 29 Dec 2010 20:52:02 +0000 Subject: [PATCH 37/58] First implementation of a search box for the tags browser --- src/calibre/gui2/tag_view.py | 120 +++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 7d3b82e00c..a5763346fc 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -10,20 +10,20 @@ Browsing book collection by tags. from itertools import izip from functools import partial -from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \ - QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ +from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \ + QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox,\ QAbstractItemModel, QVariant, QModelIndex, QMenu, \ - QPushButton, QWidget, QItemDelegate + QPushButton, QWidget, QItemDelegate, QLineEdit from calibre.ebooks.metadata import title_sort from calibre.gui2 import config, NONE from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.library.database2 import Tag from calibre.utils.config import tweaks -from calibre.utils.icu import sort_key, upper +from calibre.utils.icu import sort_key, upper, lower from calibre.utils.search_query_parser import saved_searches from calibre.utils.formatter import eval_formatter -from calibre.gui2 import error_dialog +from calibre.gui2 import error_dialog, warning_dialog from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_list_editor import TagListEditor @@ -54,6 +54,8 @@ class TagDelegate(QItemDelegate): # {{{ painter.setClipRect(r) # Paint the text + if item.boxed: + painter.drawRoundedRect(r, 5, 5) r.setLeft(r.left()+r.height()+3) painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, model.data(index, Qt.DisplayRole).toString()) @@ -357,6 +359,7 @@ class TagTreeItem(object): # {{{ parent=None, tooltip=None, category_key=None): self.parent = parent self.children = [] + self.boxed = False if self.parent is not None: self.parent.append(self) if data is None: @@ -940,6 +943,79 @@ class TagsModel(QAbstractItemModel): # {{{ ans.append('%s%s:"=%s"'%(prefix, category, tag.name)) return ans + def find_node(self, txt, start_index): + if not txt: + return None + txt = lower(txt) + if start_index is None: + start_index = QModelIndex() + self.node_found = None + + def process_tag(depth, tag_index, tag_item, start_path): + path = self.path_for_index(tag_index) + if depth < len(start_path) and path[depth] <= start_path[depth]: + return False + tag = tag_item.tag + if tag is None: + return False + if lower(tag.name).find(txt) >= 0: + self.node_found = tag_index + return True + return False + + def process_level(depth, category_index, start_path): + path = self.path_for_index(category_index) + if depth < len(start_path): + if path[depth] < start_path[depth]: + return False + if path[depth] > start_path[depth]: + start_path = path + for j in xrange(self.rowCount(category_index)): + tag_index = self.index(j, 0, category_index) + tag_item = tag_index.internalPointer() + if tag_item.type == TagTreeItem.CATEGORY: + if process_level(depth+1, tag_index, start_path): + return True + else: + if process_tag(depth+1, tag_index, tag_item, start_path): + return True + return False + + for i in xrange(self.rowCount(QModelIndex())): + if process_level(0, self.index(i, 0, QModelIndex()), + self.path_for_index(start_index)): + break + return self.node_found + + def show_item_at_index(self, idx, box=False): + if idx.isValid(): + tag_item = idx.internalPointer() + self.tags_view.setCurrentIndex(idx) + self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter) + if box: + tag_item.boxed = True + self.dataChanged.emit(idx, idx) + + def clear_boxed(self): + def process_tag(tag_index, tag_item): + if tag_item.boxed: + tag_item.boxed = False + self.dataChanged.emit(tag_index, tag_index) + + def process_level(category_index): + for j in xrange(self.rowCount(category_index)): + tag_index = self.index(j, 0, category_index) + tag_item = tag_index.internalPointer() + if tag_item.type == TagTreeItem.CATEGORY: + process_level(tag_index) + else: + process_tag(tag_index, tag_item) + + for i in xrange(self.rowCount(QModelIndex())): + process_level(self.index(i, 0, QModelIndex())) + + + # }}} class TagBrowserMixin(object): # {{{ @@ -1059,6 +1135,24 @@ class TagBrowserWidget(QWidget): # {{{ self.setLayout(self._layout) self._layout.setContentsMargins(0,0,0,0) + search_layout = QHBoxLayout() + self._layout.addLayout(search_layout) + self.item_search = QLineEdit(parent) + try: + self.item_search.setPlaceholderText(_('Find item in tag browser')) + except: + # Using Qt < 4.7 + pass + search_layout.addWidget(self.item_search) + self.search_button = QPushButton() + self.search_button.setText(_('Find!')) + self.search_button.setFixedWidth(40) + search_layout.addWidget(self.search_button) + self.current_position = None + self.search_button.clicked.connect(self.find) + self.item_search.editingFinished.connect(self.find) + self.item_search.textChanged.connect(self.find_text_changed) + parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.addWidget(parent.tags_view) @@ -1093,6 +1187,22 @@ class TagBrowserWidget(QWidget): # {{{ def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) + def find_text_changed(self, str): + self.current_position = None + + def find(self): + self.search_button.setFocus(True) + model = self.tags_view.model() + model.clear_boxed() + self.current_position =\ + model.find_node(unicode(self.item_search.text()), self.current_position) + if self.current_position: + model.show_item_at_index(self.current_position, box=True) + elif self.item_search.text(): + warning_dialog(self.tags_view, _('No item found'), + _('No (more) matches for that search')).exec_() + + # }}} From c54b2f6776c8dfb288d803c47762536d7480082e Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 29 Dec 2010 20:25:15 -0500 Subject: [PATCH 38/58] Add Output Encoding option (present on command line) to GUI for TXT, PDB and PMLZ Output formats. --- src/calibre/ebooks/pdf/writer.py | 2 +- src/calibre/ebooks/txt/output.py | 3 +-- src/calibre/gui2/convert/pdb_output.py | 2 +- src/calibre/gui2/convert/pdb_output.ui | 14 ++++++++++++-- src/calibre/gui2/convert/pml_output.py | 3 ++- src/calibre/gui2/convert/pmlz_output.ui | 16 +++++++++++++--- src/calibre/gui2/convert/txt_output.py | 3 ++- src/calibre/gui2/convert/txt_output.ui | 22 ++++++++++++++++------ 8 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index 2b4ba35d3e..7af0ed05c9 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -57,7 +57,7 @@ def get_pdf_printer(opts, for_comic=False): h = opts.output_profile.comic_screen_size[1] if for_comic else \ opts.output_profile.height dpi = opts.output_profile.dpi - printer.setPaperSize(QSizeF(float(w) / dpi, float(h)/dpi), QPrinter.Inch) + printer.setPaperSize(QSizeF(float(w) / dpi, float(h) / dpi), QPrinter.Inch) printer.setPageMargins(opts.margin_left, opts.margin_top, opts.margin_right, opts.margin_bottom, QPrinter.Point) printer.setOrientation(orientation(opts.orientation)) diff --git a/src/calibre/ebooks/txt/output.py b/src/calibre/ebooks/txt/output.py index a6369b6f0b..0e077672d8 100644 --- a/src/calibre/ebooks/txt/output.py +++ b/src/calibre/ebooks/txt/output.py @@ -29,8 +29,7 @@ class TXTOutput(OutputFormatPlugin): OptionRecommendation(name='output_encoding', recommended_value='utf-8', level=OptionRecommendation.LOW, help=_('Specify the character encoding of the output document. ' \ - 'The default is utf-8. Note: This option is not honored by all ' \ - 'formats.')), + 'The default is utf-8.')), OptionRecommendation(name='inline_toc', recommended_value=False, level=OptionRecommendation.LOW, help=_('Add Table of Contents to beginning of the book.')), diff --git a/src/calibre/gui2/convert/pdb_output.py b/src/calibre/gui2/convert/pdb_output.py index 9f88656f2f..51c202cb03 100644 --- a/src/calibre/gui2/convert/pdb_output.py +++ b/src/calibre/gui2/convert/pdb_output.py @@ -19,7 +19,7 @@ class PluginWidget(Widget, Ui_Form): ICON = I('mimetypes/unknown.png') def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, ['format', 'inline_toc']) + Widget.__init__(self, parent, ['format', 'inline_toc', 'output_encoding']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/pdb_output.ui b/src/calibre/gui2/convert/pdb_output.ui index 772a19b79e..17bdc0a984 100644 --- a/src/calibre/gui2/convert/pdb_output.ui +++ b/src/calibre/gui2/convert/pdb_output.ui @@ -27,7 +27,7 @@ - + Qt::Vertical @@ -40,13 +40,23 @@ - + &Inline TOC + + + + Output Encoding: + + + + + +
diff --git a/src/calibre/gui2/convert/pml_output.py b/src/calibre/gui2/convert/pml_output.py index 61207d3de5..f7905194ca 100644 --- a/src/calibre/gui2/convert/pml_output.py +++ b/src/calibre/gui2/convert/pml_output.py @@ -17,6 +17,7 @@ class PluginWidget(Widget, Ui_Form): ICON = I('mimetypes/unknown.png') def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, ['inline_toc', 'full_image_depth']) + Widget.__init__(self, parent, ['inline_toc', 'full_image_depth', + 'output_encoding']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/pmlz_output.ui b/src/calibre/gui2/convert/pmlz_output.ui index 3573e14210..9754752c8a 100644 --- a/src/calibre/gui2/convert/pmlz_output.ui +++ b/src/calibre/gui2/convert/pmlz_output.ui @@ -14,7 +14,7 @@ Form
- + Qt::Vertical @@ -27,20 +27,30 @@ - + &Inline TOC - + Do not reduce image size and depth + + + + Output Encoding: + + + + + + diff --git a/src/calibre/gui2/convert/txt_output.py b/src/calibre/gui2/convert/txt_output.py index 2fafad4b43..9f30e0d83f 100644 --- a/src/calibre/gui2/convert/txt_output.py +++ b/src/calibre/gui2/convert/txt_output.py @@ -21,7 +21,8 @@ class PluginWidget(Widget, Ui_Form): def __init__(self, parent, get_option, get_help, db=None, book_id=None): Widget.__init__(self, parent, ['newline', 'max_line_length', 'force_max_line_length', - 'inline_toc', 'markdown_format', 'keep_links', 'keep_image_references']) + 'inline_toc', 'markdown_format', 'keep_links', 'keep_image_references', + 'output_encoding']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/txt_output.ui b/src/calibre/gui2/convert/txt_output.ui index 19e4ec52a1..6290a096c8 100644 --- a/src/calibre/gui2/convert/txt_output.ui +++ b/src/calibre/gui2/convert/txt_output.ui @@ -27,7 +27,7 @@ - + Qt::Vertical @@ -40,7 +40,7 @@ - + &Inline TOC @@ -60,34 +60,44 @@ - + Force maximum line length - + Apply Markdown formatting to text - + Do not remove links (<a> tags) before processing - + Do not remove image references before processing + + + + Output Encoding: + + + + + + From ced43993b7139b60ecce034cd6ef3fee95872101 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 30 Dec 2010 10:57:27 +0000 Subject: [PATCH 39/58] Improvements to the tag browser find feature --- src/calibre/gui2/tag_view.py | 45 ++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index a5763346fc..96b8719b09 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -13,7 +13,7 @@ from functools import partial from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \ QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox,\ QAbstractItemModel, QVariant, QModelIndex, QMenu, \ - QPushButton, QWidget, QItemDelegate, QLineEdit + QPushButton, QWidget, QItemDelegate, QString from calibre.ebooks.metadata import title_sort from calibre.gui2 import config, NONE @@ -28,6 +28,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog +from calibre.gui2.widgets import HistoryLineEdit class TagDelegate(QItemDelegate): # {{{ @@ -725,7 +726,7 @@ class TagsModel(QAbstractItemModel): # {{{ category_icon = category_node.icon, category_key=category_node.category_key) else: - if upper(tag.name[0]) != collapse_letter: + if upper(tag.sort[0]) != collapse_letter: collapse_letter = upper(tag.name[0]) sub_cat = TagTreeItem(parent=category, data = collapse_letter, @@ -943,7 +944,7 @@ class TagsModel(QAbstractItemModel): # {{{ ans.append('%s%s:"=%s"'%(prefix, category, tag.name)) return ans - def find_node(self, txt, start_index): + def find_node(self, key, txt, start_index): if not txt: return None txt = lower(txt) @@ -970,6 +971,8 @@ class TagsModel(QAbstractItemModel): # {{{ return False if path[depth] > start_path[depth]: start_path = path + if key and category_index.internalPointer().category_key != key: + return False for j in xrange(self.rowCount(category_index)): tag_index = self.index(j, 0, category_index) tag_item = tag_index.internalPointer() @@ -1131,13 +1134,14 @@ class TagBrowserWidget(QWidget): # {{{ def __init__(self, parent): QWidget.__init__(self, parent) + self.parent = parent self._layout = QVBoxLayout() self.setLayout(self._layout) self._layout.setContentsMargins(0,0,0,0) search_layout = QHBoxLayout() self._layout.addLayout(search_layout) - self.item_search = QLineEdit(parent) + self.item_search = HistoryLineEdit(parent) try: self.item_search.setPlaceholderText(_('Find item in tag browser')) except: @@ -1150,8 +1154,10 @@ class TagBrowserWidget(QWidget): # {{{ search_layout.addWidget(self.search_button) self.current_position = None self.search_button.clicked.connect(self.find) - self.item_search.editingFinished.connect(self.find) - self.item_search.textChanged.connect(self.find_text_changed) + self.item_search.initialize('tag_browser_search') + self.item_search.lineEdit().returnPressed.connect(self.find_text_changed) + self.item_search.activated[QString].connect(self.find_text_changed) + self.item_search.completer().setCaseSensitivity(Qt.CaseSensitive) parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view @@ -1187,15 +1193,36 @@ class TagBrowserWidget(QWidget): # {{{ def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) - def find_text_changed(self, str): + def find_text_changed(self, str=None): + print 'here', str self.current_position = None + self.find() def find(self): self.search_button.setFocus(True) model = self.tags_view.model() model.clear_boxed() - self.current_position =\ - model.find_node(unicode(self.item_search.text()), self.current_position) + txt = unicode(self.item_search.currentText()) + + idx = self.item_search.findText(txt, Qt.MatchFixedString) + self.item_search.blockSignals(True) + if idx < 0: + self.item_search.insertItem(0, txt) + else: + t = self.item_search.itemText(idx) + self.item_search.removeItem(idx) + self.item_search.insertItem(0, t) + self.item_search.setCurrentIndex(0) + self.item_search.blockSignals(False) + + colon = txt.find(':') + key = None + if colon > 0: + key = self.parent.library_view.model().db.\ + field_metadata.search_term_to_field_key(txt[:colon]) + txt = txt[colon+1:] + print key, txt + self.current_position = model.find_node(key, txt, self.current_position) if self.current_position: model.show_item_at_index(self.current_position, box=True) elif self.item_search.text(): From f69f6a3dae1288c507c29d9d640b1c92306b6330 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 30 Dec 2010 12:42:46 +0000 Subject: [PATCH 40/58] This time, really fix the sorting problem. --- src/calibre/gui2/library/__init__.py | 2 +- src/calibre/gui2/library/models.py | 7 ++++--- src/calibre/gui2/library/views.py | 8 ++++---- src/calibre/library/caches.py | 5 ++++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/calibre/gui2/library/__init__.py b/src/calibre/gui2/library/__init__.py index d7180de99a..e1344101ec 100644 --- a/src/calibre/gui2/library/__init__.py +++ b/src/calibre/gui2/library/__init__.py @@ -7,4 +7,4 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import Qt -DEFAULT_SORT = ('timestamp', Qt.DescendingOrder) +DEFAULT_SORT = ('timestamp', False) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 22a9db0fef..49cb1ce182 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -247,12 +247,13 @@ class BooksModel(QAbstractTableModel): # {{{ if not self.db: return self.about_to_be_sorted.emit(self.db.id) - ascending = order == Qt.AscendingOrder + if not isinstance(order, bool): + order = order == Qt.AscendingOrder label = self.column_map[col] - self.db.sort(label, ascending) + self.db.sort(label, order) if reset: self.reset() - self.sorted_on = (label, order == Qt.AscendingOrder) + self.sorted_on = (label, order) self.sort_history.insert(0, self.sorted_on) self.sorting_done.emit(self.db.index) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 457cfaf754..322199a4f9 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -165,7 +165,7 @@ class BooksView(QTableView): # {{{ partial(self.column_header_context_handler, action='descending', column=col)) if self._model.sorted_on[0] == col: - ac = a if self._model.sorted_on[1] == Qt.AscendingOrder else d + ac = a if self._model.sorted_on[1] else d ac.setCheckable(True) ac.setChecked(True) if col not in ('ondevice', 'rating', 'inlibrary') and \ @@ -282,13 +282,13 @@ class BooksView(QTableView): # {{{ def cleanup_sort_history(self, sort_history): history = [] for col, order in sort_history: + if not isinstance(order, bool): + continue if col == 'date': col = 'timestamp' if col in self.column_map: - if (not history or history[0][0] != col): + if (not history or history[-1][0] != col): history.append([col, order]) - elif isinstance(order, bool) and history[0][1] != order: - history[0][1] = order return history def apply_sort_history(self, saved_history): diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index ff3aa0bf67..a32c45191f 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -669,6 +669,9 @@ class ResultCache(SearchQueryParser): # {{{ fields = [('timestamp', False)] keyg = SortKeyGenerator(fields, self.field_metadata, self._data) + # For efficiency, the key generator returns a plain value if only one + # field is in the sort field list. Because the normal cmp function will + # always assume asc, we must deal with asc/desc here. if len(fields) == 1: self._map.sort(key=keyg, reverse=not fields[0][1]) else: @@ -697,7 +700,7 @@ class SortKeyGenerator(object): def __init__(self, fields, field_metadata, data): from calibre.utils.icu import sort_key self.field_metadata = field_metadata - self.orders = [-1 if x[1] else 1 for x in fields] + self.orders = [1 if x[1] else -1 for x in fields] self.entries = [(x[0], field_metadata[x[0]]) for x in fields] self.library_order = tweaks['title_series_sorting'] == 'library_order' self.data = data From e1ff235aed08e6b6678c442a2476a4f0e3ed8ada Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 30 Dec 2010 14:50:05 +0000 Subject: [PATCH 41/58] books_plugin_data API. --- src/calibre/library/database2.py | 32 ++++++++++++++++++++++++-- src/calibre/library/schema_upgrades.py | 28 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index c50d1669e5..cd3c44387b 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en' ''' The database used to store ebook metadata ''' -import os, sys, shutil, cStringIO, glob, time, functools, traceback, re +import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, json from itertools import repeat from math import ceil from Queue import Queue @@ -32,7 +32,7 @@ from calibre.customize.ui import run_plugins_on_import from calibre import isbytestring from calibre.utils.filenames import ascii_filename from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp -from calibre.utils.config import prefs, tweaks +from calibre.utils.config import prefs, tweaks, from_json, to_json from calibre.utils.icu import sort_key from calibre.utils.search_query_parser import saved_searches, set_saved_searches from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format @@ -2700,6 +2700,34 @@ books_series_link feeds return duplicates + def add_custom_book_data(self, book_id, name, val): + x = self.conn.get('SELECT id FROM books WHERE ID=?', (book_id,), all=False) + if x is None: + raise ValueError('add_custom_book_data: no such book_id %d'%book_id) + # Do the json encode first, in case it throws an exception + s = json.dumps(val, default=to_json) + self.conn.execute('DELETE FROM books_plugin_data WHERE book=? AND name=?', + (book_id, name)) + self.conn.execute('''INSERT INTO books_plugin_data(book, name, val) + VALUES(?, ?, ?)''', (book_id, name, s)) + self.commit() + + def get_custom_book_data(self, book_id, name, default=None): + try: + s = self.conn.get('''select val FROM books_plugin_data + WHERE book=? AND name=?''', (book_id, name), all=False) + if s is None: + return default + return json.loads(s, object_hook=from_json) + except: + pass + return default + + def delete_custom_book_data(self, book_id, name): + self.conn.execute('DELETE FROM books_plugin_data WHERE book=? AND name=?', + (book_id, name)) + self.commit() + def get_custom_recipes(self): for id, title, script in self.conn.get('SELECT id,title,script FROM feeds'): yield id, title, script diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index 1483743e4a..0b7a3f5350 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -441,3 +441,31 @@ class SchemaUpgrade(object): WHERE id=NEW.id AND OLD.title <> NEW.title; END; ''') + + def upgrade_version_17(self): + 'custom book data table (for plugins)' + script = ''' + DROP TABLE IF EXISTS books_plugin_data; + CREATE TABLE books_plugin_data(id INTEGER PRIMARY KEY, + book INTEGER NON NULL, + name TEXT NON NULL, + val TEXT NON NULL, + UNIQUE(book,name)); + DROP TRIGGER IF EXISTS books_delete_trg; + CREATE TRIGGER books_delete_trg + AFTER DELETE ON books + BEGIN + DELETE FROM books_authors_link WHERE book=OLD.id; + DELETE FROM books_publishers_link WHERE book=OLD.id; + DELETE FROM books_ratings_link WHERE book=OLD.id; + DELETE FROM books_series_link WHERE book=OLD.id; + DELETE FROM books_tags_link WHERE book=OLD.id; + DELETE FROM data WHERE book=OLD.id; + DELETE FROM comments WHERE book=OLD.id; + DELETE FROM conversion_options WHERE book=OLD.id; + DELETE FROM books_plugin_data WHERE book=OLD.id; + END; + ''' + self.conn.executescript(script) + + From afd18eec88a963e76de355911fc0e75518f252a8 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 30 Dec 2010 15:31:49 +0000 Subject: [PATCH 42/58] Tags browser find: Get rid of two print statements. Adjust the boxing rectangle to make it more visible. --- src/calibre/gui2/tag_view.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 96b8719b09..7ad5060256 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -56,7 +56,7 @@ class TagDelegate(QItemDelegate): # {{{ # Paint the text if item.boxed: - painter.drawRoundedRect(r, 5, 5) + painter.drawRoundedRect(r.adjusted(1,1,-1,-1), 5, 5) r.setLeft(r.left()+r.height()+3) painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, model.data(index, Qt.DisplayRole).toString()) @@ -1194,7 +1194,6 @@ class TagBrowserWidget(QWidget): # {{{ self.tags_view.set_pane_is_visible(to_what) def find_text_changed(self, str=None): - print 'here', str self.current_position = None self.find() @@ -1221,7 +1220,6 @@ class TagBrowserWidget(QWidget): # {{{ key = self.parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) txt = txt[colon+1:] - print key, txt self.current_position = model.find_node(key, txt, self.current_position) if self.current_position: model.show_item_at_index(self.current_position, box=True) From 138c323f2d7165ae4e4b9bf0dcd9bc05767c0678 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 30 Dec 2010 15:53:11 +0000 Subject: [PATCH 43/58] Tags browser find: put placeholder text back. Add tooltips. --- src/calibre/gui2/tag_view.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 7ad5060256..573b3bd217 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -1143,13 +1143,19 @@ class TagBrowserWidget(QWidget): # {{{ self._layout.addLayout(search_layout) self.item_search = HistoryLineEdit(parent) try: - self.item_search.setPlaceholderText(_('Find item in tag browser')) + self.item_search.lineEdit().setPlaceholderText(_('Find item in tag browser')) except: # Using Qt < 4.7 pass + self.item_search.setToolTip(_( + 'Search for items. This is a "contains" search; items containing the\n' + 'text anywhere in the name will be found. You can limit the search\n' + 'to particular categories using syntax similar to search. For example,\n' + 'tags:foo will find foo in any tag, but not in authors etc.')) search_layout.addWidget(self.item_search) self.search_button = QPushButton() self.search_button.setText(_('Find!')) + self.search_button.setToolTip(_('Find the first/next matching item')) self.search_button.setFixedWidth(40) search_layout.addWidget(self.search_button) self.current_position = None From 6931d463d4219327ddd1d14eee8c401e52f75150 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 09:41:49 -0700 Subject: [PATCH 44/58] Fix #8088 (Support for Pocketbook Pro 603) --- src/calibre/devices/eb600/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/eb600/driver.py b/src/calibre/devices/eb600/driver.py index 246b753fa8..de8455e595 100644 --- a/src/calibre/devices/eb600/driver.py +++ b/src/calibre/devices/eb600/driver.py @@ -230,7 +230,7 @@ class POCKETBOOK301(USBMS): class POCKETBOOK602(USBMS): name = 'PocketBook Pro 602/902 Device Interface' - description = _('Communicate with the PocketBook 602 reader.') + description = _('Communicate with the PocketBook 602/603/902 reader.') author = 'Kovid Goyal' supported_platforms = ['windows', 'osx', 'linux'] FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm', @@ -244,7 +244,7 @@ class POCKETBOOK602(USBMS): BCD = [0x0324] VENDOR_NAME = '' - WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB902'] + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB603', 'PB902'] class POCKETBOOK701(USBMS): From 60558691303d249328a025a8fb30a6c4e58910cf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 09:46:25 -0700 Subject: [PATCH 45/58] Fix #8111 (Feature request: Ability to choose working temp folder) --- src/calibre/manual/customize.rst | 1 + src/calibre/ptempfile.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst index d7b4e931d9..6218bf8112 100644 --- a/src/calibre/manual/customize.rst +++ b/src/calibre/manual/customize.rst @@ -21,6 +21,7 @@ Environment variables ----------------------- * ``CALIBRE_CONFIG_DIRECTORY`` - sets the directory where configuration files are stored/read. + * ``CALIBRE_TEMP_DIR`` - sets the temporary directory used by calibre * ``CALIBRE_OVERRIDE_DATABASE_PATH`` - allows you to specify the full path to metadata.db. Using this variable you can have metadata.db be in a location other than the library folder. Useful if your library folder is on a networked drive that does not support file locking. * ``CALIBRE_DEVELOP_FROM`` - Used to run from a calibre development environment. See :ref:`develop`. * ``CALIBRE_OVERRIDE_LANG`` - Used to force the language used by the interface (ISO 639 language code) diff --git a/src/calibre/ptempfile.py b/src/calibre/ptempfile.py index 71ae9b0789..ac7df1c4e3 100644 --- a/src/calibre/ptempfile.py +++ b/src/calibre/ptempfile.py @@ -40,7 +40,7 @@ def base_dir(): _base_dir = td else: _base_dir = tempfile.mkdtemp(prefix='%s_%s_tmp_'%(__appname__, - __version__)) + __version__), dir=os.environ.get('CALIBRE_TEMP_DIR', None)) atexit.register(remove_dir, _base_dir) return _base_dir From 89b3f22cd4d1a2544b606f369a074df70b750d8d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 10:15:16 -0700 Subject: [PATCH 46/58] Update The Week --- .../recipes/the_week_magazine_free.recipe | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/resources/recipes/the_week_magazine_free.recipe b/resources/recipes/the_week_magazine_free.recipe index 1bac4133e7..6e033eaf82 100644 --- a/resources/recipes/the_week_magazine_free.recipe +++ b/resources/recipes/the_week_magazine_free.recipe @@ -1,17 +1,19 @@ - __license__ = 'GPL v3' -__copyright__ = '2010, Darko Miletic ' +__copyright__ = '2010, JOlo' ''' www.theweek.com ''' from calibre.web.feeds.news import BasicNewsRecipe +import re -class TheWeekFree(BasicNewsRecipe): - title = 'The Week Magazine - Free content' - __author__ = 'Darko Miletic' +class TheWeek(BasicNewsRecipe): + title = 'The Week Magazine' + __author__ = 'Jim Olo' description = "The best of the US and international media. Daily coverage of commentary and analysis of the day's events, as well as arts, entertainment, people and gossip, and political cartoons." publisher = 'The Week Publications, Inc.' + masthead_url = 'http://test.theweek.com/images/logo_theweek.gif' + cover_url = masthead_url category = 'news, politics, USA' oldest_article = 7 max_articles_per_feed = 100 @@ -19,31 +21,27 @@ class TheWeekFree(BasicNewsRecipe): encoding = 'utf-8' use_embedded_content = False language = 'en' + preprocess_regexps = [(re.compile(r'

', re.DOTALL), lambda match: '')] + remove_tags_before = dict(name='h1') + remove_tags_after = dict(name='div', attrs={'class':'articleSubscribe4free'}) + remove_tags = [ + dict(name='div', attrs={'class':['floatLeft','imageCaption','slideshowImageAttribution','postDate','utilities','cartoonInfo','left','middle','col300','articleSubscribe4free',' articleFlyout','articleFlyout floatRight','fourFreeBar']}) + ,dict(name='div', attrs={'id':['cartoonThumbs','rightColumn','header','partners']}) + ,dict(name='ul', attrs={'class':['slideshowNav','hotTopicsList topicList']}) + ] + remove_attributes = ['width','height', 'style', 'font', 'color'] + extra_css = ''' + h1{font-family:Geneva, Arial, Helvetica, sans-serif;color:#154B7A;} + h3{font-size: 14px;color:#999999; font-family:Geneva, Arial, Helvetica, sans-serif;font-weight: bold;} + h2{color:#666666; font-family:Geneva, Arial, Helvetica, sans-serif;font-size:small;} + p {font-family:Arial,Helvetica,sans-serif;} + ''' + filter_regexps = [r'www\.palmcoastdata\.com'] - conversion_options = { - 'comment' : description - , 'tags' : category - , 'publisher' : publisher - , 'language' : language - } - - keep_only_tags = [ - dict(name=['h1','h2']) - , dict(name='div', attrs={'class':'basefont'}) - , dict(name='div', attrs={'id':'slideshowLoader'}) - ] - - remove_tags = [ - dict(name='div', attrs={'id':['digg_dugg','articleRight','dateHeader']}) - ,dict(name=['object','embed','iframe']) - ] - - - feeds = [ - (u'News & Opinions' , u'http://www.theweek.com/section/index/news_opinion.rss') - ,(u'Arts & Leisure' , u'http://www.theweek.com/section/index/arts_leisure.rss') - ,(u'Business' , u'http://www.theweek.com/section/index/business.rss' ) - ,(u'Cartoon & Short takes' , u'http://www.theweek.com/section/index/cartoons_wit.rss') - ] - + feeds = [ + (u'News-Opinion', u'http://theweek.com/section/index/news_opinion.rss'), + (u'Business', u'http://theweek.com/section/index/business.rss'), + (u'Arts-Life', u'http://theweek.com/section/index/arts_life.rss'), + (u'Cartoons', u'http://theweek.com/section/index/cartoon_wit/0/all-cartoons.rss') + ] From 6147d566b0ce01efe46a7e9f8dcd9181d0997dcf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 10:16:41 -0700 Subject: [PATCH 47/58] Updated Salon --- resources/recipes/salon.recipe | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/resources/recipes/salon.recipe b/resources/recipes/salon.recipe index ed7ec98f10..c421ab094d 100644 --- a/resources/recipes/salon.recipe +++ b/resources/recipes/salon.recipe @@ -25,22 +25,20 @@ class Salon_com(BasicNewsRecipe): feeds = [ ('News & Politics', 'http://feeds.salon.com/salon/news'), - ('War Room', 'http://feeds.salon.com/salon/war_room'), - ('Arts & Entertainment', 'http://feeds.salon.com/salon/ent'), - ('I Like to Watch', 'http://feeds.salon.com/salon/iltw'), - ('Beyond Multiplex', 'http://feeds.salon.com/salon/btm'), - ('Book Reviews', 'http://feeds.salon.com/salon/books'), - ('All Life', 'http://feeds.salon.com/salon/mwt'), - ('All Opinion', 'http://feeds.salon.com/salon/opinion'), - ('Glenn Greenwald', 'http://feeds.salon.com/salon/greenwald'), - ('Garrison Keillor', 'http://dir.salon.com/topics/garrison_keillor/index.rss'), - ('Joan Walsh', 'http://www.salon.com/rss/walsh.rss'), - ('All Sports', 'http://feeds.salon.com/salon/sports'), + ('War Room', 'http://feeds.feedburner.com/salon/war_room'), + ('Joan Walsh', 'http://feeds.feedburner.com/Salon_Joan_Walsh'), + ('Glenn Greenwald', 'http://feeds.feedburner.com/salon/greenwald'), ('Tech & Business', 'http://feeds.salon.com/salon/tech'), - ('How World Works', 'http://feeds.salon.com/salon/htww') + ('Ask the Pilot', 'http://feeds.feedburner.com/salon/ask_the_pilot'), + ('How World Works', 'http://feeds.feedburner.com/salon/htww'), + ('Life', 'http://feeds.feedburner.com/salon/mwt'), + ('Broadsheet', 'http://feeds.feedburner.com/salon/broadsheet'), + ('Movie Reviews', 'http://feeds.feedburner.com/salon/movie_reviews'), + ('Film Salon', 'http://feeds.feedburner.com/Salon/Film_Salon'), + ('TV', 'http://feeds.feedburner.com/salon/tv'), + ('Books', 'http://feeds.feedburner.com/salon/books') ] def print_version(self, url): return url.replace('/index.html', '/print.html') - From 0e44baa99709dedb817d4dbd15abd7915f639dc0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 30 Dec 2010 17:19:37 +0000 Subject: [PATCH 48/58] Improvements on tags search. Add '*x' which restricts the tags browser to those matching x. Clean up some signal handling. --- src/calibre/gui2/tag_view.py | 54 ++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 573b3bd217..fe9726d8a9 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -336,12 +336,13 @@ class TagsView(QTreeView): # {{{ # If the number of user categories changed, if custom columns have come or # gone, or if columns have been hidden or restored, we must rebuild the # model. Reason: it is much easier than reconstructing the browser tree. - def set_new_model(self): + def set_new_model(self, filter_categories_by = None): try: self._model = TagsModel(self.db, parent=self, hidden_categories=self.hidden_categories, search_restriction=self.search_restriction, - drag_drop_finished=self.drag_drop_finished) + drag_drop_finished=self.drag_drop_finished, + filter_categories_by=filter_categories_by) self.setModel(self._model) except: # The DB must be gone. Set the model to None and hope that someone @@ -461,7 +462,8 @@ class TagTreeItem(object): # {{{ class TagsModel(QAbstractItemModel): # {{{ def __init__(self, db, parent, hidden_categories=None, - search_restriction=None, drag_drop_finished=None): + search_restriction=None, drag_drop_finished=None, + filter_categories_by=None): QAbstractItemModel.__init__(self, parent) # must do this here because 'QPixmap: Must construct a QApplication @@ -481,6 +483,7 @@ class TagsModel(QAbstractItemModel): # {{{ self.hidden_categories = hidden_categories self.search_restriction = search_restriction self.row_map = [] + self.filter_categories_by = filter_categories_by # get_node_tree cannot return None here, because row_map is empty data = self.get_node_tree(config['sort_tags_by']) @@ -648,6 +651,11 @@ class TagsModel(QAbstractItemModel): # {{{ else: data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) + if self.filter_categories_by: + for category in data.keys(): + data[category] = [t for t in data[category] + if lower(t.name).find(self.filter_categories_by) >= 0] + tb_categories = self.db.field_metadata for category in tb_categories: if category in data: # The search category can come and go @@ -948,7 +956,7 @@ class TagsModel(QAbstractItemModel): # {{{ if not txt: return None txt = lower(txt) - if start_index is None: + if start_index is None or not start_index.isValid(): start_index = QModelIndex() self.node_found = None @@ -1017,7 +1025,8 @@ class TagsModel(QAbstractItemModel): # {{{ for i in xrange(self.rowCount(QModelIndex())): process_level(self.index(i, 0, QModelIndex())) - + def get_filter_categories_by(self): + return self.filter_categories_by # }}} @@ -1151,7 +1160,9 @@ class TagBrowserWidget(QWidget): # {{{ 'Search for items. This is a "contains" search; items containing the\n' 'text anywhere in the name will be found. You can limit the search\n' 'to particular categories using syntax similar to search. For example,\n' - 'tags:foo will find foo in any tag, but not in authors etc.')) + 'tags:foo will find foo in any tag, but not in authors etc. Entering\n' + '*foo will filter all categories at once, showing only those items\n' + 'containing the text "foo"')) search_layout.addWidget(self.item_search) self.search_button = QPushButton() self.search_button.setText(_('Find!')) @@ -1161,8 +1172,9 @@ class TagBrowserWidget(QWidget): # {{{ self.current_position = None self.search_button.clicked.connect(self.find) self.item_search.initialize('tag_browser_search') - self.item_search.lineEdit().returnPressed.connect(self.find_text_changed) - self.item_search.activated[QString].connect(self.find_text_changed) + self.item_search.lineEdit().returnPressed.connect(self.do_find) + self.item_search.lineEdit().textEdited.connect(self.find_text_changed) + self.item_search.activated[QString].connect(self.do_find) self.item_search.completer().setCaseSensitivity(Qt.CaseSensitive) parent.tags_view = TagsView(parent) @@ -1199,18 +1211,34 @@ class TagBrowserWidget(QWidget): # {{{ def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) - def find_text_changed(self, str=None): + def find_text_changed(self, str): + self.current_position = None + + def do_find(self, str=None): self.current_position = None self.find() def find(self): - self.search_button.setFocus(True) model = self.tags_view.model() model.clear_boxed() - txt = unicode(self.item_search.currentText()) + txt = unicode(self.item_search.currentText()).strip() + + if txt.startswith('*'): + self.tags_view.set_new_model(filter_categories_by=txt[1:]) + self.current_position = None + return + if model.get_filter_categories_by(): + self.tags_view.set_new_model(filter_categories_by=None) + self.current_position = None + model = self.tags_view.model() + + if not txt: + return - idx = self.item_search.findText(txt, Qt.MatchFixedString) self.item_search.blockSignals(True) + self.item_search.lineEdit().blockSignals(True) + self.search_button.setFocus(True) + idx = self.item_search.findText(txt, Qt.MatchFixedString) if idx < 0: self.item_search.insertItem(0, txt) else: @@ -1219,6 +1247,7 @@ class TagBrowserWidget(QWidget): # {{{ self.item_search.insertItem(0, t) self.item_search.setCurrentIndex(0) self.item_search.blockSignals(False) + self.item_search.lineEdit().blockSignals(False) colon = txt.find(':') key = None @@ -1226,6 +1255,7 @@ class TagBrowserWidget(QWidget): # {{{ key = self.parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) txt = txt[colon+1:] + self.current_position = model.find_node(key, txt, self.current_position) if self.current_position: model.show_item_at_index(self.current_position, box=True) From a2b2364c1c4dda3508b1bec1fb696befe2213ec0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 10:35:30 -0700 Subject: [PATCH 49/58] Fix #8103 (Driver for Pocketbook 903) --- src/calibre/devices/eb600/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/eb600/driver.py b/src/calibre/devices/eb600/driver.py index de8455e595..3201229699 100644 --- a/src/calibre/devices/eb600/driver.py +++ b/src/calibre/devices/eb600/driver.py @@ -230,7 +230,7 @@ class POCKETBOOK301(USBMS): class POCKETBOOK602(USBMS): name = 'PocketBook Pro 602/902 Device Interface' - description = _('Communicate with the PocketBook 602/603/902 reader.') + description = _('Communicate with the PocketBook 602/603/902/903 reader.') author = 'Kovid Goyal' supported_platforms = ['windows', 'osx', 'linux'] FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm', @@ -244,7 +244,7 @@ class POCKETBOOK602(USBMS): BCD = [0x0324] VENDOR_NAME = '' - WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB603', 'PB902'] + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB603', 'PB902', 'PB903'] class POCKETBOOK701(USBMS): From cadbd6ba5488b5fd92eba597ac7d63dd40e6be92 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 12:01:35 -0700 Subject: [PATCH 50/58] Fix #8115 (Unsupported encoding Windows-1251) --- src/calibre/ebooks/fb2/input.py | 3 +++ src/calibre/ebooks/metadata/fb2.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/fb2/input.py b/src/calibre/ebooks/fb2/input.py index 2b08a716cc..1f9a3ffe95 100644 --- a/src/calibre/ebooks/fb2/input.py +++ b/src/calibre/ebooks/fb2/input.py @@ -41,9 +41,12 @@ class FB2Input(InputFormatPlugin): from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.oeb.base import XLINK_NS, XHTML_NS, RECOVER_PARSER + from calibre.ebooks.chardet import xml_to_unicode NAMESPACES = {'f':FB2NS, 'l':XLINK_NS} log.debug('Parsing XML...') raw = stream.read().replace('\0', '') + raw = xml_to_unicode(raw, strip_encoding_pats=True, + assume_utf8=True)[0] try: doc = etree.fromstring(raw) except etree.XMLSyntaxError: diff --git a/src/calibre/ebooks/metadata/fb2.py b/src/calibre/ebooks/metadata/fb2.py index 3636b89df4..2d6192f949 100644 --- a/src/calibre/ebooks/metadata/fb2.py +++ b/src/calibre/ebooks/metadata/fb2.py @@ -9,6 +9,7 @@ import mimetypes, os from base64 import b64decode from lxml import etree from calibre.ebooks.metadata import MetaInformation +from calibre.ebooks.chardet import xml_to_unicode XLINK_NS = 'http://www.w3.org/1999/xlink' def XLINK(name): @@ -23,7 +24,10 @@ def get_metadata(stream): tostring = lambda x : etree.tostring(x, method='text', encoding=unicode).strip() parser = etree.XMLParser(recover=True, no_network=True) - root = etree.fromstring(stream.read(), parser=parser) + raw = stream.read() + raw = xml_to_unicode(raw, strip_encoding_pats=True, + assume_utf8=True)[0] + root = etree.fromstring(raw, parser=parser) authors, author_sort = [], None for au in XPath('//fb2:author')(root): fname = lname = author = None From 42f790e8e5eac08920bcefbd789511a5922e69d0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 17:26:45 -0700 Subject: [PATCH 51/58] Karlsruhe News by tfeld --- resources/recipes/karlsruhe.recipe | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 resources/recipes/karlsruhe.recipe diff --git a/resources/recipes/karlsruhe.recipe b/resources/recipes/karlsruhe.recipe new file mode 100644 index 0000000000..c0bc5369f1 --- /dev/null +++ b/resources/recipes/karlsruhe.recipe @@ -0,0 +1,52 @@ +import re +from calibre.web.feeds.news import BasicNewsRecipe + +class KANewsRecipe(BasicNewsRecipe): + title = u'KA-News.de' + description = u'Nachrichten aus Karlsruhe, Deutschland und der Welt.' + __author__ = 'tfeld' + lang='de' + no_stylesheets = True + + oldest_article = 7 + max_articles_per_feed = 100 + + feeds = [ + (u'News aus Karlsruhe', 'http://www.ka-news.de/storage/rss/rss/karlsruhe.xml'), + (u'Kulturnachrichten aus Karlsruhe', 'http://www.ka-news.de/storage/rss/rss/kultur.xml'), + (u'Durlach: News aus Durlach', 'http://www.ka-news.de/storage/rss/rss/durlach.xml'), + (u'Stutensee: News aus Stutensee Blankenloch, Büchig, Friedrichstal, Staffort, Spöck', 'http://www.ka-news.de/storage/rss/rss/stutensee.xml'), + (u'Bruchsal: News aus Bruchsal', 'http://www.ka-news.de/storage/rss/rss/bruchsal.xml'), + (u'Wirtschaftsnews aus Karlsruhe', 'http://www.ka-news.de/storage/rss/rss/wirtschaft.xml'), + (u'ka-news.de - Sport', 'http://www.ka-news.de/storage/rss/rss/sport.xml'), + (u'KSC-News - News rund um den KSC', 'http://www.ka-news.de/storage/rss/rss/ksc.xml'), + (u'ka-news.de - BG Karlsruhe', 'http://www.ka-news.de/storage/rss/rss/basketball.xml') + ] + + preprocess_regexps = [ + (re.compile(r'width:[0-9]*?px', re.DOTALL|re.IGNORECASE), lambda match: ''), + ] + + remove_tags_before = dict(id='artdetail_ueberschrift') + remove_tags_after = dict(id='artdetail_unterzeile') + remove_tags = [dict(name=['div'], attrs={'class': 'lbx_table'}), + dict(name=['div'], attrs={'class': 'lk_zumthema'}), + dict(name=['div'], attrs={'class': 'lk_thumb'}), + dict(name=['div'], attrs={'class': 'lk_trenner'}), + dict(name=['div'], attrs={'class': 'lupen_container'}), + dict(name=['script']), + dict(name=['span'], attrs={'style': 'display:none;'}), + dict(name=['span'], attrs={'class': 'comm_info'}), + dict(name=['h3'], attrs={'id': 'artdetail_unterzeile'})] + + # removing style attribute _after_ removing specifig tags above + remove_attributes = ['width','height','style'] + + extra_css = ''' + h1{ font-size:large; font-weight:bold; } + h2{ font-size:medium; font-weight:bold; } + ''' + + def get_cover_url(self): + return 'http://www.ka-news.de/storage/scl/techkanews/logos/434447_m1t1w250q75s1v29681_ka-news-Logo_mit_Schatten_transparent.png' + From df27288e8830bdc9eb517a6bf8cffc1d5c0dda92 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 17:32:43 -0700 Subject: [PATCH 52/58] Fix #8122 (Nextbook2 or Nextbook3 Ebook Reader driver needed) --- src/calibre/customize/builtins.py | 3 ++- src/calibre/devices/misc.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index aea0e340c4..d0f986209c 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -478,7 +478,7 @@ from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \ from calibre.devices.sne.driver import SNE from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \ GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR, \ - TREKSTOR, EEEREADER + TREKSTOR, EEEREADER, NEXTBOOK from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.kobo.driver import KOBO from calibre.devices.bambook.driver import BAMBOOK @@ -606,6 +606,7 @@ plugins += [ BAMBOOK, TREKSTOR, EEEREADER, + NEXTBOOK, ITUNES, ] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index d4776ecca7..2a0fdf6433 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -264,3 +264,23 @@ class EEEREADER(USBMS): VENDOR_NAME = 'LINUX' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET' +class NEXTBOOK(USBMS): + + name = 'Nextbook device interface' + gui_name = 'Nextbook' + description = _('Communicate with the Nextbook Reader') + author = 'Kovid Goyal' + supported_platforms = ['windows', 'osx', 'linux'] + + # Ordered list of supported formats + FORMATS = ['epub', 'fb2', 'txt', 'pdf'] + + VENDOR_ID = [0x05e3] + PRODUCT_ID = [0x0726] + BCD = [0x021a] + + EBOOK_DIR_MAIN = '' + + VENDOR_NAME = 'NEXT2' + WINDOWS_MAIN_MEM = '1.0.14' + From 7d3fb20c6b3745559571e0acf3052b19faef1d30 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 22:52:04 -0700 Subject: [PATCH 53/58] Fix #8097 (Some Kindle azw books have svg code which disrupts Calibre viewer) --- src/calibre/ebooks/mobi/reader.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 02abc51cd3..14e3ed11c3 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -513,11 +513,14 @@ class MobiReader(object): mobi_version = self.book_header.mobi_version for x in root.xpath('//ncx'): x.getparent().remove(x) + svg_tags = [] for i, tag in enumerate(root.iter(etree.Element)): tag.attrib.pop('xmlns', '') for x in tag.attrib: if ':' in x: del tag.attrib[x] + if tag.tag and barename(tag.tag) == 'svg': + svg_tags.append(tag) if tag.tag and barename(tag.tag.lower()) in \ ('country-region', 'place', 'placetype', 'placename', 'state', 'city', 'street', 'address', 'content', 'form'): @@ -628,6 +631,11 @@ class MobiReader(object): cls = cls + (' ' if cls else '') + ncls attrib['class'] = cls + for tag in svg_tags: + p = tag.getparent() + if hasattr(p, 'remove'): + p.remove(tag) + def create_opf(self, htmlfile, guide=None, root=None): mi = getattr(self.book_header.exth, 'mi', self.embedded_mi) if mi is None: From 31a9f2369af525a29577600ee26d2a846d7b542b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 22:53:27 -0700 Subject: [PATCH 54/58] Fix #8085 (Please add support for Samsung Tab) --- src/calibre/devices/android/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index ced6a45da4..6387b48857 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -64,7 +64,8 @@ class ANDROID(USBMS): WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', - 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE'] + 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', + 'SGH-T849'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD'] From 373176c91260aa7f3535b72c8953707260963e74 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 22:57:51 -0700 Subject: [PATCH 55/58] Fix #7929 (Samsung Galaxy tab) --- src/calibre/customize/profiles.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 54c4259678..abfd6f67ce 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -439,6 +439,13 @@ class TabletOutput(iPadOutput): screen_size = (sys.maxint, sys.maxint) comic_screen_size = (sys.maxint, sys.maxint) +class SamsungGalaxy(TabletOutput): + name = 'Samsung Galaxy' + shortname = 'galaxy' + description = _('Intended for the Samsung Galaxy and similar tablet devices with ' + 'a resolution of 600x1280') + screen_size = comic_screen_size = (600, 1280) + class SonyReaderOutput(OutputProfile): name = 'Sony Reader' From 2e1d63816f6ca20b2171a92c333f258a4f089bd1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 22:58:42 -0700 Subject: [PATCH 56/58] ... --- src/calibre/customize/profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index abfd6f67ce..177f482aa6 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -714,7 +714,7 @@ class BambookOutput(OutputProfile): output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output, SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output, HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput, - iPadOutput, KoboReaderOutput, TabletOutput, + iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy, SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput, IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput, BambookOutput, NookColorOutput] From 5ff65409916851ef1583015c2fb51c644be2c402 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 23:12:19 -0700 Subject: [PATCH 57/58] Updated El Universal. Fixes #8059 (Downloaded newspapers in device and E-book viewer have text and link problems) --- resources/recipes/el_universal.recipe | 104 ++++---------------------- 1 file changed, 15 insertions(+), 89 deletions(-) diff --git a/resources/recipes/el_universal.recipe b/resources/recipes/el_universal.recipe index 1995d0f932..f053812c05 100644 --- a/resources/recipes/el_universal.recipe +++ b/resources/recipes/el_universal.recipe @@ -1,7 +1,5 @@ -#!/usr/bin/env python - __license__ = 'GPL v3' -__copyright__ = '2009, Darko Miletic ' +__copyright__ = '2009-2010, Darko Miletic ' ''' eluniversal.com.mx ''' @@ -18,75 +16,25 @@ class ElUniversal(BasicNewsRecipe): category = 'news, politics, Mexico' no_stylesheets = True use_embedded_content = False - encoding = 'cp1252' + encoding = 'utf8' remove_javascript = True - language = 'es' + remove_empty_feeds = True + publication_type = 'newspaper' + language = 'es' extra_css = ''' - body{font-family:Arial,Helvetica,sans-serif; font-size:x-small;} - .geoGris30{font-family:Georgia,"Times New Roman",Times,serif; font-size:large; color:#003366; font-weight:bold;} - .arnegro16{font-family:Georgia,"Times New Roman",Times,serif; font-weight:bold; font-size:small;} - .tbazull2{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; color:#336699; font-size:xx-small;} - .tbgrisf11{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; color: #666666; font-size:xx-small;} - .verrojo13{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; color: #CC0033; font-size:xx-small;} - .trnegro13{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; font-size:xx-small;} - .txt-fotogaleria{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; font-size:xx-small;} + body{font-family:Arial,Helvetica,sans-serif} + .noteTitle{font-family: Georgia,"Times New Roman",Times,serif; color: #336699; font-size: xx-large; font-weight: bold} + .noteInfo{display: block; color: gray} ''' - keep_only_tags = [ dict(name='table', attrs={'width':"633"}),dict(name='table', attrs={'width':"629"}),] - + keep_only_tags = [ dict(name='div', attrs={'id':'noteContent'})] + remove_tags_after = dict(attrs={'class':'noteText'}) remove_tags = [ - dict(name='table', attrs={'bgcolor':"#f5f5f5"}), - dict(name='td', attrs={'bgcolor':"#f7f8f9"}), - dict(name='td', attrs={'bgcolor':"#f5f5f5"}), - dict(name='table', attrs={'width':"302"}), - dict(name='table', attrs={'width':"214"}), - dict(name='table', attrs={'width':"112"}), - dict(name='table', attrs={'width':"980"}), - dict(name='td', attrs={'height':"1"}), - dict(name='td', attrs={'height':"4"}), - dict(name='td', attrs={'height':"20"}), - dict(name='td', attrs={'height':"10"}), - dict(name='td', attrs={'class':["trrojo11","trbris11","trrojo12","arrojo12s","tbazul13"]}), - dict(name='div', attrs={'id':["mapg","ver_off_todosloscom","todosloscom"]}), - dict(name='span', attrs={'class':["trazul18b","trrojo11","trnaranja11","trbris11","georojo18b","geogris18"]}), - dict(name='span', attrs={'class':["detalles-opinion"]}), - dict(name='a', attrs={'class':["arnaranja12b","trbris11","arazul12rel","trrojo10"]}), - dict(name='img', src = "/img/icono_imprimir.gif"), - dict(name='img', src = "/img/icono_enviar_mail.gif"), - dict(name='img', src = "/img/icono_fuente_g.gif"), - dict(name='img', src = "/img/icono_fuente_m.gif"), - dict(name='img', src = "/img/icono_fuente_c.gif"), - dict(name='img', src = "/img/icono_compartir.gif"), - dict(name='img', src = "/img/icono_enviar_coment.gif"), - dict(name='img', src = "http://www.eluniversal.com.mx/n_img/bot-notasrel.gif"), - dict(name='img', src = "http://www.eluniversal.com.mx/n_img/fr.gif"), - dict(name='img', src = "/img/espiral2.gif"), - dict(name='img', src = "http://www.eluniversal.com.mx/n_img/b"), - dict(name='img', src = "/img/icono_enviar_coment.gifot-notasrel.gif"), - dict(name='img', src = "/n_img/icono_tipo3.gif"), - dict(name='img', src = "/n_img/icono_tipo2.gif"), - dict(name='img', src = "/n_img/icono_print.gif"), - dict(name='img', src = "/n_img/icono_mail2.gif"), - dict(name='img', src = "/n_img/im-comentarios-2a.gif"), - dict(name='img', src = "/n_img/im-comentarios-1a.gif"), - dict(name='img', src = "/img/icono_coment.gif"), - dict(name='img', src = "http://www.eluniversal.com.mx/n_img/bot-sitiosrel.gif"), - dict(name='img', src = "/n_img/icono_tipomenos.gif"), - dict(name='img', src = "/img/futbol/19.jpg"), - dict(name='img', alt = "Facebook"), - dict(name='img', alt = "Twitter"), - dict(name='img', alt = "Google"), - dict(name='img', alt = "LinkedIn"), - dict(name='img', alt = "Viadeo"), - dict(name='img', alt = "Digg"), - dict(name='img', alt = "Delicious"), - dict(name='img', alt = "Meneame"), - dict(name='img', alt = "Yahoo"), - dict(name='img', alt = "Technorati"), - dict(name='a',text =["Compartir","Facebook","Twitter","Google","LinkedIn","Viadeo","Digg","Delicious","Meneame","Yahoo","Technorati"]), - dict(name='select'), - dict(name='a', attrs={'class':"tbgriscompartir"}), - ] + dict(attrs={'class':'noteExtras'}), + dict(name=['meta','iframe','base','embed','object']), + dict(attrs={'id':'tm_box'}) + ] + remove_attributes=['lang','onclick'] feeds = [ (u'Minuto por Minuto', u'http://www.eluniversal.com.mx/rss/universalmxm.xml' ) @@ -101,25 +49,3 @@ class ElUniversal(BasicNewsRecipe): ,(u'Computacion' , u'http://www.eluniversal.com.mx/rss/computo.xml' ) ,(u'Sociedad' , u'http://www.eluniversal.com.mx/rss/sociedad.xml' ) ] - - # def print_version(self, url): - # return url.replace('/notas/','/notas/vi_') - - def preprocess_html(self, soup): - mtag = '' - soup.head.insert(0,mtag) - for tag in soup.findAll(name='td',attrs={'class': 'arazul50'}): - tag.insert(0,"

") - tag.insert(2,"

") - - return soup - - def postprocess_html(self, soup,first): - - for tag in soup.findAll(name=['table', 'span','i']): - tag.name = 'div' - for item in soup.findAll(align = "right"): - del item['align'] - - return soup - From 82ffe9d10775e52852385d3454f3650bbebfcd46 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Dec 2010 23:26:25 -0700 Subject: [PATCH 58/58] Add a tweak to control the delay when sending mails using gmail or hotmail. Fixes #8064 (Email 5 Min. Added to Options) --- resources/default_tweaks.py | 8 ++++++++ src/calibre/gui2/email.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index a2a9a0a043..7eafc5b357 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -315,3 +315,11 @@ locale_for_sorting = '' # metadata one book at a time. If True, then the fields are laid out using two # columns. If False, one column is used. metadata_single_use_2_cols_for_custom_fields = True + +# The number of seconds to wait before sending emails when using a +# public email server like gmail or hotmail. Default is: 5 minutes +# Setting it to lower may cause the server's SPAM controls to kick in, +# making email sending fail. Changes will take effect only after a restart of +# calibre. +public_smtp_relay_delay = 301 + diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py index 2911222ba8..6b2ed81413 100644 --- a/src/calibre/gui2/email.py +++ b/src/calibre/gui2/email.py @@ -22,6 +22,7 @@ from calibre.customize.ui import available_input_formats, available_output_forma from calibre.ebooks.metadata import authors_to_string from calibre.constants import preferred_encoding from calibre.gui2 import config, Dispatcher, warning_dialog +from calibre.utils.config import tweaks class EmailJob(BaseJob): # {{{ @@ -83,7 +84,7 @@ class Emailer(Thread): # {{{ rh = opts.relay_host if rh and ( 'gmail.com' in rh or 'live.com' in rh): - self.rate_limit = 301 + self.rate_limit = tweaks['public_smtp_relay_delay'] def stop(self): self._run = False