From 60b5d3853fc41cd17111287bfa2fff9b6bcab096 Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Fri, 14 Jan 2011 07:24:07 +0900 Subject: [PATCH 01/36] fix nikkei_sub economy --- resources/recipes/nikkei_sub_economy.recipe | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/recipes/nikkei_sub_economy.recipe b/resources/recipes/nikkei_sub_economy.recipe index 2dd8f1add8..8e7a68dfe7 100644 --- a/resources/recipes/nikkei_sub_economy.recipe +++ b/resources/recipes/nikkei_sub_economy.recipe @@ -27,6 +27,9 @@ class NikkeiNet_sub_economy(BasicNewsRecipe): {'class':"JSID_basePageMove JSID_baseAsyncSubmit cmn-form_area JSID_optForm_utoken"}, {'class':"cmn-article_keyword cmn-clearfix"}, {'class':"cmn-print_headline cmn-clearfix"}, + {'class':"cmn-article_list"}, + dict(id="ABOUT-NIKKEI"), + {'class':"cmn-sub_market"}, ] remove_tags_after = {'class':"cmn-pr_list"} From a21bf30ff8207ec9a4e644d1ffbfcbee58a346c6 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 16 Jan 2011 08:41:16 +0000 Subject: [PATCH 02/36] 1) finish JSON-storage of template function source code 2) fix for #8388. This fix was chosen because it emulates the behavior in 0.7.38, where get_metadata returned empty author lists --- .../gui2/preferences/template_functions.py | 15 +++++++++++++-- src/calibre/library/database2.py | 5 ++++- src/calibre/library/sqlite.py | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py index 2e16b0f4c3..8ffd65b2b5 100644 --- a/src/calibre/gui2/preferences/template_functions.py +++ b/src/calibre/gui2/preferences/template_functions.py @@ -5,7 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import traceback +import json, traceback from calibre.gui2 import error_dialog from calibre.gui2.preferences import ConfigWidgetBase, test_widget @@ -73,6 +73,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.textBrowser.setHtml(help_text) def initialize(self): + try: + with open(P('template-functions.json'), 'rb') as f: + self.builtin_source_dict = json.load(f, encoding='utf-8') + except: + self.builtin_source_dict = {} + self.funcs = formatter_functions.get_functions() self.builtins = formatter_functions.get_builtins() @@ -179,8 +185,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): func = self.funcs[txt] self.argument_count.setValue(func.arg_count) self.documentation.setText(func.doc) - self.program.setPlainText(func.program_text) if txt in self.builtins: + if hasattr(func, 'program_text'): + self.program.setPlainText(func.program_text) + elif txt in self.builtin_source_dict: + self.program.setPlainText(self.builtin_source_dict[txt]) + else: + self.program.setPlainText(_('function source code not available')) self.documentation.setReadOnly(True) self.argument_count.setReadOnly(True) self.program.setReadOnly(True) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 3a2109e01e..df094347b8 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -690,7 +690,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): mi = Metadata(None) aut_list = row[fm['au_map']] - aut_list = [p.split(':::') for p in aut_list.split(':#:')] + if not aut_list: + aut_list = [] + else: + aut_list = [p.split(':::') for p in aut_list.split(':#:')] aum = [] aus = {} for (author, author_sort) in aut_list: diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index 83f19b8711..622d6b8459 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -100,7 +100,7 @@ class AumSortedConcatenate(object): keys = self.ans.keys() l = len(keys) if l == 0: - return 'Unknown:::Unknown' + return None if l == 1: return self.ans[keys[0]] return ':#:'.join([self.ans[v] for v in sorted(keys)]) From e0b2d0b62a6b1f4c4a2f37cdcdffa5e1744b892b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 16 Jan 2011 10:34:05 +0000 Subject: [PATCH 03/36] Add function documentation to template editing dialog (F2 on a composite column) --- src/calibre/gui2/dialogs/template_dialog.py | 40 +++++++++- src/calibre/gui2/dialogs/template_dialog.ui | 81 ++++++++++++++++----- 2 files changed, 103 insertions(+), 18 deletions(-) diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 60d4025ef9..62accdc842 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -3,8 +3,11 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __license__ = 'GPL v3' +import json + from PyQt4.Qt import Qt, QDialog, QDialogButtonBox from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog +from calibre.utils.formatter_functions import formatter_functions class TemplateDialog(QDialog, Ui_TemplateDialog): @@ -17,9 +20,44 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) + self.textbox.setTabStopWidth(10) + self.source_code.setTabStopWidth(10) + self.documentation.setReadOnly(True) + self.source_code.setReadOnly(True) + if text is not None: self.textbox.setPlainText(text) - self.textbox.setTabStopWidth(50) self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) + try: + with open(P('template-functions.json'), 'rb') as f: + self.builtin_source_dict = json.load(f, encoding='utf-8') + except: + self.builtin_source_dict = {} + + self.funcs = formatter_functions.get_functions() + self.builtins = formatter_functions.get_builtins() + + func_names = sorted(self.funcs) + self.function.clear() + self.function.addItem('') + self.function.addItems(func_names) + self.function.setCurrentIndex(0) + self.function.currentIndexChanged[str].connect(self.function_changed) + + print self.textbox.tabStopWidth() + print self.source_code.tabStopWidth() + + def function_changed(self, toWhat): + name = unicode(toWhat) + self.source_code.clear() + self.documentation.clear() + if name in self.funcs: + self.documentation.setPlainText(self.funcs[name].doc) + if name in self.builtins: + if name in self.builtin_source_dict: + self.source_code.setPlainText(self.builtin_source_dict[name]) + else: + self.source_code.setPlainText(self.funcs[name].program_text) + diff --git a/src/calibre/gui2/dialogs/template_dialog.ui b/src/calibre/gui2/dialogs/template_dialog.ui index a30d6ef273..e1980a8397 100644 --- a/src/calibre/gui2/dialogs/template_dialog.ui +++ b/src/calibre/gui2/dialogs/template_dialog.ui @@ -6,8 +6,8 @@ 0 0 - 500 - 235 + 588 + 546 @@ -19,21 +19,68 @@ Edit Comments - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + Function name: + + + + + + + + + + Documentation: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Python code: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + 16777215 + 75 + + + + + + + + + + From 4a9e8bcb2fd0d60438e3265defb358891de6bc75 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 16 Jan 2011 10:48:43 +0000 Subject: [PATCH 04/36] Remove some print statements. --- src/calibre/gui2/dialogs/template_dialog.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 62accdc842..174056ef80 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -46,9 +46,6 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.function.setCurrentIndex(0) self.function.currentIndexChanged[str].connect(self.function_changed) - print self.textbox.tabStopWidth() - print self.source_code.tabStopWidth() - def function_changed(self, toWhat): name = unicode(toWhat) self.source_code.clear() From 5c4154bb0d37a273e4925c2daa4dc15b4e9f752b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 16 Jan 2011 11:26:34 +0000 Subject: [PATCH 05/36] Make date.format_date render UNDEFINED_DATE as ''. This makes composite columns and templates behave in the same fashion as the GUI has for some time. --- src/calibre/utils/date.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/utils/date.py b/src/calibre/utils/date.py index f025a0c9bf..2551b90788 100644 --- a/src/calibre/utils/date.py +++ b/src/calibre/utils/date.py @@ -148,6 +148,9 @@ def format_date(dt, format, assume_utc=False, as_utc=False): if len(mo.group(0)) == 2: return '%02d'%(dt.year % 100) return '%04d'%dt.year + if dt == UNDEFINED_DATE: + return '' + format = re.sub('d{1,4}', format_day, format) format = re.sub('M{1,4}', format_month, format) return re.sub('yyyy|yy', format_year, format) From e131f99db8e5a80a68e4e28a152848effb7ece76 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Jan 2011 09:18:37 -0700 Subject: [PATCH 06/36] Cleanup layout of bulk metadata edit dialog --- src/calibre/gui2/dialogs/metadata_bulk.ui | 236 +++++++++++----------- 1 file changed, 115 insertions(+), 121 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 9240cd1af8..d52bc2cb89 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -75,13 +75,31 @@ - - - - A&utomatically set author sort + + + + true + + + + + + A&utomatically set author sort + + + + + + + &Swap title and author + + + + + @@ -95,7 +113,7 @@ - + Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles. @@ -115,7 +133,7 @@ - + Rating of this book. 0-5 stars @@ -156,7 +174,7 @@ - + true @@ -220,7 +238,7 @@ Check this box to remove all tags from the books. - Remove all + Remove &all @@ -241,52 +259,35 @@ - - - - - List of known series. You can add new series. - - - List of known series. You can add new series. - - - true - - - QComboBox::InsertAlphabetically - - - QComboBox::AdjustToContents - - - - - - - If checked, the series will be cleared - - - Clear series - - - - - - - Qt::Horizontal - - - - 20 - 0 - - - - - + + + List of known series. You can add new series. + + + List of known series. You can add new series. + + + true + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + - + + + + If checked, the series will be cleared + + + &Clear series + + + + @@ -297,7 +298,7 @@ you selected them. So if you selected Book A and then Book B, Book A will have series number 1 and Book B series number 2. - Automatically number books in this series + &Automatically number books in this series @@ -312,7 +313,7 @@ for that series. Checking this box will tell calibre to start numbering from the value in the box - Force numbers to start with + &Force numbers to start with: @@ -332,19 +333,6 @@ from the value in the box - - - - Qt::Horizontal - - - - 20 - 10 - - - - @@ -358,59 +346,56 @@ from the value in the box - - - - - - true - - - - - - - &Swap title and author - - - - - - - Force the title to be in title case. If both this and swap authors are checked, -title and author are swapped before the title case is set - - - Change title to title case - - - - - - - Remove stored conversion settings for the selected books. - -Future conversion of these books will use the default settings. - - - Remove &stored conversion settings for the selected books - - - - - - - Qt::Vertical - - + + - 20 - 40 + 120 + 16777215 - + - + + + + + + Force the title to be in title case. If both this and swap authors are checked, +title and author are swapped before the title case is set + + + Change title to title &case + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Remove stored conversion settings for the selected books. + +Future conversion of these books will use the default settings. + + + Remove &stored conversion settings for the selected books + + + + + + Change &cover @@ -440,6 +425,19 @@ Future conversion of these books will use the default settings. + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -902,14 +900,10 @@ not multiple and the destination field is multiple remove_tags remove_all_tags series - clear_series autonumber_series series_numbering_restarts series_start_number remove_format - remove_conversion_settings - swap_title_and_author - change_title_to_title_case button_box search_field search_mode From b83b89ce7437a8e0b7547cf6ac8a27112ee243ae Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Jan 2011 10:09:14 -0700 Subject: [PATCH 07/36] Fix #7568 (Allow bulk editing of Published date) --- src/calibre/gui2/dialogs/metadata_bulk.py | 31 ++- src/calibre/gui2/dialogs/metadata_bulk.ui | 108 ++++++-- src/calibre/gui2/dialogs/metadata_single.py | 13 +- src/calibre/gui2/dialogs/metadata_single.ui | 287 ++++++++++---------- 4 files changed, 274 insertions(+), 165 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 5ea8f00148..da6e92c26a 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -15,15 +15,16 @@ from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.ebooks.metadata.book.base import composite_formatter from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2.custom_column_widgets import populate_metadata_page -from calibre.gui2 import error_dialog, ResizableDialog +from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE from calibre.gui2.progress_indicator import ProgressIndicator from calibre.utils.config import dynamic from calibre.utils.titlecase import titlecase from calibre.utils.icu import sort_key, capitalize -from calibre.utils.config import prefs +from calibre.utils.config import prefs, tweaks from calibre.utils.magick.draw import identify_data +from calibre.utils.date import qt_to_dt -def get_cover_data(path): +def get_cover_data(path): # {{{ old = prefs['read_file_metadata'] if not old: prefs['read_file_metadata'] = True @@ -46,7 +47,7 @@ def get_cover_data(path): prefs['read_file_metadata'] = old return cdata, area - +# }}} class MyBlockingBusy(QDialog): # {{{ @@ -132,7 +133,8 @@ class MyBlockingBusy(QDialog): # {{{ remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \ do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_remove_conv, do_auto_author, series, do_series_restart, \ - series_start_value, do_title_case, cover_action, clear_series = self.args + series_start_value, do_title_case, cover_action, clear_series, \ + pubdate = self.args # first loop: do author and title. These will commit at the end of each @@ -209,6 +211,9 @@ class MyBlockingBusy(QDialog): # {{{ if clear_series: self.db.set_series(id, '', notify=False, commit=False) + if pubdate is not None: + self.db.set_pubdate(id, pubdate, notify=False, commit=False) + if do_series: if do_series_restart: if self.series_start_value is None: @@ -288,6 +293,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.series.editTextChanged.connect(self.series_changed) self.tag_editor_button.clicked.connect(self.tag_editor) self.autonumber_series.stateChanged[int].connect(self.auto_number_changed) + self.pubdate.setMinimumDate(UNDEFINED_QDATE) + pubdate_format = tweaks['gui_pubdate_display_format'] + if pubdate_format is not None: + self.pubdate.setDisplayFormat(pubdate_format) + self.pubdate.setSpecialValueText(_('Undefined')) + self.clear_pubdate_button.clicked.connect(self.clear_pubdate) if len(self.db.custom_field_keys(include_composites=False)) == 0: self.central_widget.removeTab(1) @@ -304,6 +315,9 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.central_widget.setCurrentIndex(tab) self.exec_() + def clear_pubdate(self, *args): + self.pubdate.setDate(UNDEFINED_QDATE) + def button_clicked(self, which): if which == self.button_box.button(QDialogButtonBox.Apply): self.do_again = True @@ -783,6 +797,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): do_remove_conv = self.remove_conversion_settings.isChecked() do_auto_author = self.auto_author_sort.isChecked() do_title_case = self.change_title_to_title_case.isChecked() + pubdate = None + if self.apply_pubdate.isChecked(): + pubdate = qt_to_dt(self.pubdate.date()) + cover_action = None if self.cover_remove.isChecked(): cover_action = 'remove' @@ -794,7 +812,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, do_autonumber, do_remove_format, remove_format, do_swap_ta, do_remove_conv, do_auto_author, series, do_series_restart, - series_start_value, do_title_case, cover_action, clear_series) + series_start_value, do_title_case, cover_action, clear_series, + pubdate) bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.') %len(self.ids), args, self.db, self.ids, diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index d52bc2cb89..b14c31c9d1 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -220,6 +220,9 @@ &Remove tags: + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + remove_tags @@ -260,6 +263,12 @@ + + + 0 + 0 + + List of known series. You can add new series. @@ -273,7 +282,10 @@ QComboBox::InsertAlphabetically - QComboBox::AdjustToContents + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + 40 @@ -335,27 +347,52 @@ from the value in the box - - + + - Remove &format: + &Published: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - remove_format + pubdate - - - - - 120 - 16777215 - + + + + + + MMM yyyy + + + + + + + Clear published date + + + ... + + + + :/images/trash.png:/images/trash.png + + + + + + + + + &Apply date - + @@ -395,7 +432,7 @@ Future conversion of these books will use the default settings. - + Change &cover @@ -425,7 +462,7 @@ Future conversion of these books will use the default settings. - + Qt::Vertical @@ -438,6 +475,42 @@ Future conversion of these books will use the default settings. + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 15 + + + + + + + + Remove &format: + + + remove_format + + + + + + + + 120 + 16777215 + + + + @@ -798,8 +871,8 @@ not multiple and the destination field is multiple 0 0 - 197 - 60 + 826 + 313 @@ -903,7 +976,6 @@ not multiple and the destination field is multiple autonumber_series series_numbering_restarts series_start_number - remove_format button_box search_field search_mode diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index ede605343b..e4efdf0470 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -16,7 +16,7 @@ from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QDate, \ from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \ choose_files, choose_images, ResizableDialog, \ - warning_dialog, question_dialog + warning_dialog, question_dialog, UNDEFINED_QDATE from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog from calibre.gui2.dialogs.fetch_metadata import FetchMetadata from calibre.gui2.dialogs.tag_editor import TagEditor @@ -491,11 +491,15 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.formats.setAcceptDrops(True) self.cover_changed = False self.cpixmap = None - self.pubdate.setMinimumDate(QDate(100,1,1)) + self.pubdate.setMinimumDate(UNDEFINED_QDATE) pubdate_format = tweaks['gui_pubdate_display_format'] if pubdate_format is not None: self.pubdate.setDisplayFormat(pubdate_format) - self.date.setMinimumDate(QDate(100,1,1)) + self.date.setMinimumDate(UNDEFINED_QDATE) + self.pubdate.setSpecialValueText(_('Undefined')) + self.date.setSpecialValueText(_('Undefined')) + self.clear_pubdate_button.clicked.connect(self.clear_pubdate) + self.connect(self.cover, SIGNAL('cover_changed(PyQt_PyObject)'), self.cover_dropped) QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \ @@ -615,6 +619,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.show() + def clear_pubdate(self, *args): + self.pubdate.setDate(UNDEFINED_QDATE) + def create_custom_column_editors(self): w = self.central_widget.widget(1) layout = w.layout() diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index 6d31342dcf..60c221be1a 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -100,6 +100,112 @@ + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Automatically create the title sort entry based on the current title entry. +Using this button to create title sort will change title sort from red to green. + + + ... + + + + :/images/auto_author_sort.png:/images/auto_author_sort.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Swap the author and title + + + ... + + + + :/images/swap.png:/images/swap.png + + + + 16 + 16 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Automatically create the author sort entry based on the current author entry. +Using this button to create author sort will change author sort from red to green. + + + ... + + + + :/images/auto_author_sort.png:/images/auto_author_sort.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + @@ -226,6 +332,31 @@ If the box is colored green, then text matches the individual author's sort stri + + + + + + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. + + + + + + + + + Open Tag Editor + + + Open Tag Editor + + + + :/images/chapters.png:/images/chapters.png + + + @@ -265,6 +396,20 @@ If the box is colored green, then text matches the individual author's sort stri + + + + Remove unused series (Series that have no books) + + + ... + + + + :/images/trash.png:/images/trash.png + + + @@ -330,7 +475,7 @@ If the box is colored green, then text matches the individual author's sort stri - + MMM yyyy @@ -340,144 +485,10 @@ If the box is colored green, then text matches the individual author's sort stri - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Automatically create the title sort entry based on the current title entry. -Using this button to create title sort will change title sort from red to green. - - - ... - - - - :/images/auto_author_sort.png:/images/auto_author_sort.png - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Swap the author and title - - - ... - - - - :/images/swap.png:/images/swap.png - - - - 16 - 16 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Automatically create the author sort entry based on the current author entry. -Using this button to create author sort will change author sort from red to green. - - - ... - - - - :/images/auto_author_sort.png:/images/auto_author_sort.png - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. - - - - - - - + + - Open Tag Editor - - - Open Tag Editor - - - - :/images/chapters.png:/images/chapters.png - - - - - - - Remove unused series (Series that have no books) - - - ... + Clear published date From 4e5d5bbce0ed89e1ff33836ac2222243fbb0cb24 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Jan 2011 11:45:18 -0700 Subject: [PATCH 08/36] MOBI Input: SPecial case handling of emptu div tags with a defined height used as paragraph separators. Fixes #8391 (formatting issues when converting from .azw to .mobi. Not duplicating space between paragraphs.) --- src/calibre/ebooks/mobi/reader.py | 12 +++++++++++- src/calibre/manual/faq.rst | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index e07418f41c..2f397006a1 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -542,7 +542,17 @@ class MobiReader(object): elif tag.tag == 'img': tag.set('height', height) else: - styles.append('margin-top: %s' % self.ensure_unit(height)) + if tag.tag == 'div' and not tag.text and \ + (not tag.tail or not tag.tail.strip()) and \ + not len(list(tag.iterdescendants())): + # Paragraph spacer + # Insert nbsp so that the element is never + # discarded by a renderer + tag.text = u'\u00a0' # nbsp + styles.append('height: %s' % + self.ensure_unit(height)) + else: + styles.append('margin-top: %s' % self.ensure_unit(height)) if attrib.has_key('width'): width = attrib.pop('width').strip() if width and re.search(r'\d+', width): diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 0e8c101620..ee72bf6fdb 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -450,6 +450,11 @@ How do I use purchased EPUB books with |app|? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Most purchased EPUB books have `DRM `_. This prevents |app| from opening them. You can still use |app| to store and transfer them to your e-book reader. First, you must authorize your reader on a windows machine with Adobe Digital Editions. Once this is done, EPUB books transferred with |app| will work fine on your reader. When you purchase an epub book from a website, you will get an ".acsm" file. This file should be opened with Adobe Digital Editions, which will then download the actual ".epub" e-book. The e-book file will be stored in the folder "My Digital Editions", from where you can add it to |app|. +I am getting a "Permission Denied" error? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A permission denied error can occur because of many possible reasons, none of them having anything to do with |app|. You can get permission denied errors if you are using an SD card with write protect enabled. Or if you, or some program you used changed the file permissions of the files in question to read only. Or if there is a filesystem error on the device which caused your operating system to mount the filesystem in read only mode or mark a particular file as read only pending recovery. Or if the files have their owner set to a user other than you. You will need to fix the underlying cause of the permissions error before resuming to use |app|. Read the error message carefully, see what file it points to and fix the permissions on that file. + Can I have the comment metadata show up on my reader? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 91ccd9ad749a0ef88fff14971312c77d1f3838ae Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Jan 2011 12:31:19 -0700 Subject: [PATCH 09/36] Add AZW to the default list of internall viewed formats --- src/calibre/gui2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 6a9becee50..c94b99f141 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -85,7 +85,7 @@ def _config(): c.add_opt('LRF_ebook_viewer_options', default=None, help=_('Options for the LRF ebook viewer')) c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT', - 'MOBI', 'PRC', 'HTML', 'FB2', 'PDB', 'RB', 'SNB'], + 'MOBI', 'PRC', 'AZW', 'HTML', 'FB2', 'PDB', 'RB', 'SNB'], help=_('Formats that are viewed using the internal viewer')) c.add_opt('column_map', default=ALL_COLUMNS, help=_('Columns to be displayed in the book list')) From 4d428dfa9a90023876413cab30f6d38fefdac620 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Jan 2011 12:34:49 -0700 Subject: [PATCH 10/36] Bulk metadata edit: Check apply date automatically whenever user changes the date in the pubdate field --- src/calibre/gui2/dialogs/metadata_bulk.py | 4 ++++ src/calibre/gui2/dialogs/metadata_bulk.ui | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index da6e92c26a..302766a92d 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -299,6 +299,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.pubdate.setDisplayFormat(pubdate_format) self.pubdate.setSpecialValueText(_('Undefined')) self.clear_pubdate_button.clicked.connect(self.clear_pubdate) + self.pubdate.dateChanged.connect(self.do_apply_pubdate) if len(self.db.custom_field_keys(include_composites=False)) == 0: self.central_widget.removeTab(1) @@ -315,6 +316,9 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.central_widget.setCurrentIndex(tab) self.exec_() + def do_apply_pubdate(self, *args): + self.apply_pubdate.setChecked(True) + def clear_pubdate(self, *args): self.pubdate.setDate(UNDEFINED_QDATE) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index b14c31c9d1..5690a8e555 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -367,6 +367,9 @@ from the value in the box MMM yyyy + + true + @@ -871,8 +874,8 @@ not multiple and the destination field is multiple 0 0 - 826 - 313 + 197 + 60 From 497b381bfa8ce1d5c8de65da03cc31b2b109ed10 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Jan 2011 12:45:11 -0700 Subject: [PATCH 11/36] The update found link now opens the update notification dialog instead of going straight to the download page --- src/calibre/gui2/init.py | 14 +++++++++++--- src/calibre/gui2/update.py | 9 ++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index fc70f0579d..95af265856 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -148,7 +148,6 @@ class StatusBar(QStatusBar): # {{{ self.get_version() + ' ' + _('created by Kovid Goyal') self.device_string = '' self.update_label = QLabel('') - self.update_label.setOpenExternalLinks(True) self.addPermanentWidget(self.update_label) self.update_label.setVisible(False) self._font = QFont() @@ -174,8 +173,9 @@ class StatusBar(QStatusBar): # {{{ self.clearMessage() def new_version_available(self, ver, url): - msg = (u'%s: %s') % ( - _('Update found'), url, ver) + msg = (u'%s: %s') % ( + _('Update found'), ver, ver) self.update_label.setText(msg) self.update_label.setCursor(Qt.PointingHandCursor) self.update_label.setVisible(True) @@ -240,6 +240,14 @@ class LayoutMixin(object): # {{{ self.status_bar.addPermanentWidget(button) self.status_bar.addPermanentWidget(self.jobs_button) self.setStatusBar(self.status_bar) + self.status_bar.update_label.linkActivated.connect(self.update_link_clicked) + + def update_link_clicked(self, url): + print 11111111, url + url = unicode(url) + if url.startswith('update:'): + version = url.partition(':')[-1] + self.update_found(version, force=True) def finalize_layout(self): self.status_bar.initialize(self.system_tray_icon) diff --git a/src/calibre/gui2/update.py b/src/calibre/gui2/update.py index 30cfe8f5e4..9929d50a7e 100644 --- a/src/calibre/gui2/update.py +++ b/src/calibre/gui2/update.py @@ -52,8 +52,7 @@ class UpdateNotification(QDialog): self.label = QLabel('

'+ _('%s has been updated to version %s. ' 'See the new features. Visit the download pa' - 'ge?')%(__appname__, version)) + '">new features.')%(__appname__, version)) self.label.setOpenExternalLinks(True) self.label.setWordWrap(True) self.setWindowTitle(_('Update available!')) @@ -94,13 +93,13 @@ class UpdateMixin(object): type=Qt.QueuedConnection) self.update_checker.start() - def update_found(self, version): + def update_found(self, version, force=False): os = 'windows' if iswindows else 'osx' if isosx else 'linux' url = 'http://calibre-ebook.com/download_%s'%os self.status_bar.new_version_available(version, url) - if config.get('new_version_notification') and \ - dynamic.get('update to version %s'%version, True): + if force or (config.get('new_version_notification') and \ + dynamic.get('update to version %s'%version, True)): self._update_notification__ = UpdateNotification(version, parent=self) self._update_notification__.show() From 69c6ad02110d780f131aa41bf92317b8d82838fe Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Jan 2011 12:49:46 -0700 Subject: [PATCH 12/36] ... --- src/calibre/gui2/init.py | 1 - src/calibre/manual/conversion.rst | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 95af265856..ebd670c8fa 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -243,7 +243,6 @@ class LayoutMixin(object): # {{{ self.status_bar.update_label.linkActivated.connect(self.update_link_clicked) def update_link_clicked(self, url): - print 11111111, url url = unicode(url) if url.startswith('update:'): version = url.partition(':')[-1] diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst index 4b2b169d72..71639ca749 100644 --- a/src/calibre/manual/conversion.rst +++ b/src/calibre/manual/conversion.rst @@ -547,6 +547,7 @@ Some limitations of PDF input are: * Extraction of vector images and tables from within the document is also not supported. * Some PDFs use special glyphs to represent ll or ff or fi, etc. Conversion of these may or may not work depending on just how they are represented internally in the PDF. * Some PDFs store their images upside down with a rotation instruction, |app| currently doesn't support that instruction, so the images will be rotated in the output as well. + * Links and Tables of Contents are not supported To re-iterate **PDF is a really, really bad** format to use as input. If you absolutely must use PDF, then be prepared for an output ranging anywhere from decent to unusable, depending on the input PDF. From 188a96caeb39966d0cc5e9fa1bd91e0ef6a77ac4 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 16 Jan 2011 16:40:27 -0500 Subject: [PATCH 13/36] Make TagsLineEdit into a generic CompleteLineEdit class. Use CompleteLineEdit for Author completion in the main GUI window. --- src/calibre/gui2/convert/metadata.py | 2 +- src/calibre/gui2/convert/metadata.ui | 4 +- src/calibre/gui2/custom_column_widgets.py | 12 ++-- src/calibre/gui2/dialogs/metadata_bulk.py | 8 +-- src/calibre/gui2/dialogs/metadata_bulk.ui | 6 +- src/calibre/gui2/dialogs/metadata_single.py | 4 +- src/calibre/gui2/dialogs/metadata_single.ui | 4 +- src/calibre/gui2/dialogs/search.py | 2 +- src/calibre/gui2/dialogs/search.ui | 4 +- src/calibre/gui2/library/delegates.py | 54 +++++++++++++++++- src/calibre/gui2/library/views.py | 9 ++- src/calibre/gui2/widgets.py | 62 ++++++++++++--------- src/calibre/library/database.py | 3 + 13 files changed, 118 insertions(+), 56 deletions(-) diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index d3744bb614..de03033060 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -75,7 +75,7 @@ class MetadataWidget(Widget, Ui_Form): self.publisher.setCurrentIndex(self.publisher.findText(mi.publisher)) self.author_sort.setText(mi.author_sort if mi.author_sort else '') self.tags.setText(', '.join(mi.tags if mi.tags else [])) - self.tags.update_tags_cache(self.db.all_tags()) + self.tags.update_items_cache(self.db.all_tags()) self.comment.setPlainText(mi.comments if mi.comments else '') if mi.series: self.series.setCurrentIndex(self.series.findText(mi.series)) diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui index a594f47b5d..5735193424 100644 --- a/src/calibre/gui2/convert/metadata.ui +++ b/src/calibre/gui2/convert/metadata.ui @@ -190,7 +190,7 @@ - + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. @@ -310,7 +310,7 @@

widgets.h
- TagsLineEdit + CompleteLineEdit QLineEdit
widgets.h
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index ec18675359..d80909c4bb 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -14,7 +14,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \ QPushButton from calibre.utils.date import qt_to_dt, now -from calibre.gui2.widgets import TagsLineEdit, EnComboBox +from calibre.gui2.widgets import CompleteLineEdit, EnComboBox from calibre.gui2.comments_editor import Editor as CommentsEditor from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.utils.config import tweaks @@ -212,7 +212,7 @@ class Text(Base): values = self.all_values = list(self.db.all_custom(num=self.col_id)) values.sort(key=sort_key) if self.col_metadata['is_multiple']: - w = TagsLineEdit(parent, values) + w = CompleteLineEdit(parent, values) w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) else: w = EnComboBox(parent) @@ -226,7 +226,7 @@ class Text(Base): val = self.normalize_db_val(val) if self.col_metadata['is_multiple']: self.setter(val) - self.widgets[1].update_tags_cache(self.all_values) + self.widgets[1].update_items_cache(self.all_values) else: idx = None for i, c in enumerate(self.all_values): @@ -656,7 +656,7 @@ class RemoveTags(QWidget): layout.setSpacing(5) layout.setContentsMargins(0, 0, 0, 0) - self.tags_box = TagsLineEdit(parent, values) + self.tags_box = CompleteLineEdit(parent, values) layout.addWidget(self.tags_box, stretch = 1) # self.tags_box.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) @@ -678,7 +678,7 @@ class BulkText(BulkBase): values = self.all_values = list(self.db.all_custom(num=self.col_id)) values.sort(key=sort_key) if self.col_metadata['is_multiple']: - w = TagsLineEdit(parent, values) + w = CompleteLineEdit(parent, values) w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) self.widgets = [QLabel('&'+self.col_metadata['name']+': ' + _('tags to add'), parent), w] @@ -697,7 +697,7 @@ class BulkText(BulkBase): def initialize(self, book_ids): if self.col_metadata['is_multiple']: - self.widgets[1].update_tags_cache(self.all_values) + self.widgets[1].update_items_cache(self.all_values) else: val = self.get_initial_value(book_ids) self.initial_val = val = self.normalize_db_val(val) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 302766a92d..c7d5add912 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -279,8 +279,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.changed = False all_tags = self.db.all_tags() - self.tags.update_tags_cache(all_tags) - self.remove_tags.update_tags_cache(all_tags) + self.tags.update_items_cache(all_tags) + self.remove_tags.update_items_cache(all_tags) self.initialize_combos() @@ -751,8 +751,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): if d.result() == QDialog.Accepted: tag_string = ', '.join(d.tags) self.tags.setText(tag_string) - self.tags.update_tags_cache(self.db.all_tags()) - self.remove_tags.update_tags_cache(self.db.all_tags()) + self.tags.update_items_cache(self.db.all_tags()) + self.remove_tags.update_items_cache(self.db.all_tags()) def auto_number_changed(self, state): if state: diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 5690a8e555..b826f8d48d 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -195,7 +195,7 @@
- + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. @@ -229,7 +229,7 @@ - + Comma separated list of tags to remove from the books. @@ -955,7 +955,7 @@ not multiple and the destination field is multiple
widgets.h
- TagsLineEdit + CompleteLineEdit QLineEdit
widgets.h
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index e4efdf0470..139b8a2ebe 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -556,7 +556,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): tags = self.db.tags(row) self.original_tags = ', '.join(tags.split(',')) if tags else '' self.tags.setText(self.original_tags) - self.tags.update_tags_cache(self.db.all_tags()) + self.tags.update_items_cache(self.db.all_tags()) rating = self.db.rating(row) if rating > 0: self.rating.setValue(int(rating/2.)) @@ -776,7 +776,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): if d.result() == QDialog.Accepted: tag_string = ', '.join(d.tags) self.tags.setText(tag_string) - self.tags.update_tags_cache(self.db.all_tags()) + self.tags.update_items_cache(self.db.all_tags()) def fetch_metadata(self): diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index 60c221be1a..b95267a618 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -335,7 +335,7 @@ If the box is colored green, then text matches the individual author's sort stri - + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. @@ -842,7 +842,7 @@ If the box is colored green, then text matches the individual author's sort stri
widgets.h
- TagsLineEdit + CompleteLineEdit QLineEdit
widgets.h
diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py index 62a0f8a9f1..4f72fa915e 100644 --- a/src/calibre/gui2/dialogs/search.py +++ b/src/calibre/gui2/dialogs/search.py @@ -42,7 +42,7 @@ class SearchDialog(QDialog, Ui_Dialog): self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive) all_tags = db.all_tags() - self.tags_box.update_tags_cache(all_tags) + self.tags_box.update_items_cache(all_tags) self.box_last_values = copy.deepcopy(box_values) if self.box_last_values: diff --git a/src/calibre/gui2/dialogs/search.ui b/src/calibre/gui2/dialogs/search.ui index 6848a45506..519ae8c462 100644 --- a/src/calibre/gui2/dialogs/search.ui +++ b/src/calibre/gui2/dialogs/search.ui @@ -279,7 +279,7 @@
- + Enter tags separated by spaces @@ -360,7 +360,7 @@
widgets.h
- TagsLineEdit + CompleteLineEdit QLineEdit
widgets.h
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index b41fd78dc3..af8f9c4d8a 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -16,7 +16,7 @@ from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \ QComboBox, QTextDocument from calibre.gui2 import UNDEFINED_QDATE, error_dialog -from calibre.gui2.widgets import EnLineEdit, TagsLineEdit +from calibre.gui2.widgets import EnLineEdit, CompleteLineEdit from calibre.utils.date import now, format_date from calibre.utils.config import tweaks from calibre.utils.formatter import validation_formatter @@ -173,9 +173,9 @@ class TagsDelegate(QStyledItemDelegate): # {{{ if self.db: col = index.model().column_map[index.column()] if not index.model().is_custom_column(col): - editor = TagsLineEdit(parent, self.db.all_tags()) + editor = CompleteLineEdit(parent, self.db.all_tags()) else: - editor = TagsLineEdit(parent, + editor = CompleteLineEdit(parent, sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))), key=sort_key)) return editor @@ -184,6 +184,54 @@ class TagsDelegate(QStyledItemDelegate): # {{{ return editor # }}} +class AuthorsDelegate(QStyledItemDelegate): # {{{ + def __init__(self, parent): + QStyledItemDelegate.__init__(self, parent) + self.db = None + + def set_database(self, db): + self.db = db + + def createEditor(self, parent, option, index): + if self.db: + col = index.model().column_map[index.column()] + if not index.model().is_custom_column(col): + editor = CompleteLineEdit(parent, self.db.all_author_names(), '&', True) + else: + editor = CompleteLineEdit(parent, + sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))), + key=sort_key), '&', True) + return editor + else: + editor = EnLineEdit(parent) + return editor +# }}} + +class CompleteDelegate(QStyledItemDelegate): # {{{ + def __init__(self, parent, sep, items_func_name, space_before_sep=False): + QStyledItemDelegate.__init__(self, parent) + self.sep = sep + self.items_func_name = items_func_name + self.space_before_sep = space_before_sep + + def set_database(self, db): + self.db = db + + def createEditor(self, parent, option, index): + if self.db and hasattr(self.db, self.items_func_name): + col = index.model().column_map[index.column()] + if not index.model().is_custom_column(col): + editor = CompleteLineEdit(parent, getattr(self.db, self.items_func_name)(), + self.sep, self.space_before_sep) + else: + editor = CompleteLineEdit(parent, + sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))), + key=sort_key), self.sep, self.space_before_sep) + else: + editor = EnLineEdit(parent) + return editor +# }}} + class CcDateDelegate(QStyledItemDelegate): # {{{ ''' Delegate for custom columns dates. Because this delegate stores the diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 3ff0fc3cd7..61161cd5e6 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -13,7 +13,7 @@ from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \ QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \ - TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \ + TextDelegate, DateDelegate, CompleteDelegate, CcTextDelegate, \ CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, \ CcEnumDelegate from calibre.gui2.library.models import BooksModel, DeviceBooksModel @@ -76,8 +76,8 @@ class BooksView(QTableView): # {{{ self.rating_delegate = RatingDelegate(self) self.timestamp_delegate = DateDelegate(self) self.pubdate_delegate = PubDateDelegate(self) - self.tags_delegate = TagsDelegate(self) - self.authors_delegate = TextDelegate(self) + self.tags_delegate = CompleteDelegate(self, ',', 'all_tags') + self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True) self.series_delegate = TextDelegate(self) self.publisher_delegate = TextDelegate(self) self.text_delegate = TextDelegate(self) @@ -410,8 +410,7 @@ class BooksView(QTableView): # {{{ self.save_state() self._model.set_database(db) self.tags_delegate.set_database(db) - self.authors_delegate.set_auto_complete_function( - lambda: [(x, y.replace('|', ',')) for (x, y) in db.all_authors()]) + self.authors_delegate.set_database(db) self.series_delegate.set_auto_complete_function(db.all_series) self.publisher_delegate.set_auto_complete_function(db.all_publishers) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index f2ff783a76..6e2d52f835 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -426,46 +426,47 @@ class EnLineEdit(LineEditECM, QLineEdit): pass -class TagsCompleter(QCompleter): +class ItemsCompleter(QCompleter): ''' A completer object that completes a list of tags. It is used in conjunction with a CompleterLineEdit. ''' - def __init__(self, parent, all_tags): - QCompleter.__init__(self, all_tags, parent) - self.all_tags = set(all_tags) + def __init__(self, parent, all_items): + QCompleter.__init__(self, all_items, parent) + self.all_items = set(all_items) - def update(self, text_tags, completion_prefix): - tags = list(self.all_tags.difference(text_tags)) - model = QStringListModel(tags, self) + def update(self, text_items, completion_prefix): + items = list(self.all_items.difference(text_items)) + model = QStringListModel(items, self) self.setModel(model) self.setCompletionPrefix(completion_prefix) if completion_prefix.strip() != '': self.complete() - def update_tags_cache(self, tags): - self.all_tags = set(tags) - model = QStringListModel(tags, self) + def update_items_cache(self, items): + self.all_items = set(items) + model = QStringListModel(items, self) self.setModel(model) -class TagsLineEdit(EnLineEdit): +class CompleteLineEdit(EnLineEdit): ''' A QLineEdit that can complete parts of text separated by separator. ''' - def __init__(self, parent=0, tags=[]): + def __init__(self, parent=0, complete_items=[], sep=',', space_before_sep=False): EnLineEdit.__init__(self, parent) - self.separator = ',' + self.separator = sep + self.space_before_sep = space_before_sep self.connect(self, SIGNAL('textChanged(QString)'), self.text_changed) - self.completer = TagsCompleter(self, tags) + self.completer = ItemsCompleter(self, complete_items) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.connect(self, @@ -476,32 +477,43 @@ class TagsLineEdit(EnLineEdit): self.completer.setWidget(self) - def update_tags_cache(self, tags): - self.completer.update_tags_cache(tags) + def update_items_cache(self, complete_items): + self.completer.update_items_cache(complete_items) + + def set_separator(self, sep): + self.separator = sep + + def set_space_before_sep(self, space_before): + self.space_before_sep = space_before def text_changed(self, text): all_text = unicode(text) text = all_text[:self.cursorPosition()] - prefix = text.split(',')[-1].strip() + prefix = text.split(self.separator)[-1].strip() - text_tags = [] + text_items = [] for t in all_text.split(self.separator): t1 = unicode(t).strip() if t1 != '': - text_tags.append(t) - text_tags = list(set(text_tags)) + text_items.append(t) + text_items = list(set(text_items)) self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'), - text_tags, prefix) + text_items, prefix) def complete_text(self, text): cursor_pos = self.cursorPosition() before_text = unicode(self.text())[:cursor_pos] after_text = unicode(self.text())[cursor_pos:] - prefix_len = len(before_text.split(',')[-1].strip()) - self.setText('%s%s%s %s' % (before_text[:cursor_pos - prefix_len], - text, self.separator, after_text)) - self.setCursorPosition(cursor_pos - prefix_len + len(text) + 2) + prefix_len = len(before_text.split(self.separator)[-1].strip()) + if self.space_before_sep: + complete_text_pat = '%s%s %s %s' + len_extra = 3 + else: + complete_text_pat = '%s%s%s %s' + len_extra = 2 + self.setText(complete_text_pat % (before_text[:cursor_pos - prefix_len], text, self.separator, after_text)) + self.setCursorPosition(cursor_pos - prefix_len + len(text) + len_extra) class EnComboBox(QComboBox): diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py index 6016dbd03e..e2ad8796a0 100644 --- a/src/calibre/library/database.py +++ b/src/calibre/library/database.py @@ -1059,6 +1059,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; def all_authors(self): return [ (i[0], i[1]) for i in \ self.conn.get('SELECT id, name FROM authors')] + + def all_author_names(self): + return [i[0].strip() for i in self.conn.get('SELECT name FROM authors') if i[0].strip()] def all_publishers(self): return [ (i[0], i[1]) for i in \ From cfa57f63df7e4fe8d683082e780b7df31c52fabb Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 16 Jan 2011 17:14:26 -0500 Subject: [PATCH 14/36] GUI: Have Authors combo boxes use completion using & just like tags use completion with , --- src/calibre/gui2/convert/metadata.py | 3 + src/calibre/gui2/convert/metadata.ui | 69 +++++++++------------ src/calibre/gui2/dialogs/metadata_bulk.py | 4 ++ src/calibre/gui2/dialogs/metadata_bulk.ui | 19 +++--- src/calibre/gui2/dialogs/metadata_single.py | 4 ++ src/calibre/gui2/dialogs/metadata_single.ui | 51 +++++++-------- src/calibre/gui2/dialogs/search.py | 3 + src/calibre/gui2/dialogs/search.ui | 13 ++-- src/calibre/gui2/widgets.py | 16 +++++ 9 files changed, 104 insertions(+), 78 deletions(-) diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index de03033060..5f39202e26 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -68,6 +68,9 @@ class MetadataWidget(Widget, Ui_Form): def initialize_metadata_options(self): self.initialize_combos() self.author.editTextChanged.connect(self.deduce_author_sort) + self.author.set_separator('&') + self.author.set_space_before_sep(True) + self.author.update_items_cache(self.db.all_author_names()) mi = self.db.get_metadata(self.book_id, index_is_id=True) self.title.setText(mi.title) diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui index 5735193424..24b7c0904a 100644 --- a/src/calibre/gui2/convert/metadata.ui +++ b/src/calibre/gui2/convert/metadata.ui @@ -20,38 +20,8 @@ Book Cover - - - - - - - 0 - 0 - - - - - - - - - - Use cover from &source file - - - true - - - - - 6 - - - 0 - @@ -64,12 +34,6 @@ - - 6 - - - 0 - @@ -86,7 +50,7 @@ ... - + :/images/document_open.png:/images/document_open.png @@ -95,6 +59,30 @@ + + + + Use cover from &source file + + + true + + + + + + + + + + 0 + 0 + + + + + + opt_prefer_metadata_cover @@ -255,7 +243,7 @@
- + true @@ -320,6 +308,11 @@
calibre/gui2/widgets.h
1 + + CompleteComboBox + QComboBox +
widgets.h
+
title diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index c7d5add912..2b3a319663 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -726,6 +726,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): name = name.strip().replace('|', ',') self.authors.addItem(name) self.authors.setEditText('') + + self.authors.set_separator('&') + self.authors.set_space_before_sep(True) + self.authors.update_items_cache(self.db.all_author_names()) def initialize_series(self): all_series = self.db.all_series() diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index b826f8d48d..3950026325 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -14,7 +14,7 @@ Edit Meta information - + :/images/edit_input.png:/images/edit_input.png @@ -45,7 +45,7 @@ 0 0 842 - 589 + 553 @@ -76,7 +76,7 @@
- + true @@ -210,7 +210,7 @@ Open Tag Editor - + :/images/chapters.png:/images/chapters.png @@ -381,7 +381,7 @@ from the value in the box ... - + :/images/trash.png:/images/trash.png @@ -874,8 +874,8 @@ not multiple and the destination field is multiple 0 0 - 197 - 60 + 231 + 82 @@ -964,6 +964,11 @@ not multiple and the destination field is multiple QLineEdit
widgets.h
+ + CompleteComboBox + QComboBox +
widgets.h
+
authors diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 139b8a2ebe..4ca2072317 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -724,6 +724,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): au = _('Unknown') au = ' & '.join([a.strip().replace('|', ',') for a in au.split(',')]) self.authors.setEditText(au) + + self.authors.set_separator('&') + self.authors.set_space_before_sep(True) + self.authors.update_items_cache(self.db.all_author_names()) def initialize_series(self): self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow) diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index b95267a618..b2f42937da 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -20,7 +20,7 @@ Edit Meta Information - + :/images/edit_input.png:/images/edit_input.png @@ -43,8 +43,8 @@ 0 0 - 986 - 677 + 955 + 665 @@ -125,7 +125,7 @@ Using this button to create title sort will change title sort from red to green. ... - + :/images/auto_author_sort.png:/images/auto_author_sort.png
@@ -152,7 +152,7 @@ Using this button to create title sort will change title sort from red to green. ... - + :/images/swap.png:/images/swap.png @@ -186,7 +186,7 @@ Using this button to create author sort will change author sort from red to gree ... - + :/images/auto_author_sort.png:/images/auto_author_sort.png
@@ -240,7 +240,7 @@ Using this button to create author sort will change author sort from red to gree
- + true @@ -352,7 +352,7 @@ If the box is colored green, then text matches the individual author's sort stri Open Tag Editor - + :/images/chapters.png:/images/chapters.png @@ -405,7 +405,7 @@ If the box is colored green, then text matches the individual author's sort stri ... - + :/images/trash.png:/images/trash.png @@ -491,7 +491,7 @@ If the box is colored green, then text matches the individual author's sort stri Clear published date - + :/images/trash.png:/images/trash.png @@ -550,15 +550,9 @@ If the box is colored green, then text matches the individual author's sort stri - - 6 - QLayout::SetMaximumSize - - 0 - @@ -571,19 +565,13 @@ If the box is colored green, then text matches the individual author's sort stri - - 6 - - - 0 - &Browse - + :/images/document_open.png:/images/document_open.png @@ -597,7 +585,7 @@ If the box is colored green, then text matches the individual author's sort stri T&rim - + :/images/trim.png:/images/trim.png @@ -611,7 +599,7 @@ If the box is colored green, then text matches the individual author's sort stri &Remove - + :/images/trash.png:/images/trash.png @@ -702,7 +690,7 @@ If the box is colored green, then text matches the individual author's sort stri ... - + :/images/add_book.png:/images/add_book.png @@ -722,7 +710,7 @@ If the box is colored green, then text matches the individual author's sort stri ... - + :/images/trash.png:/images/trash.png @@ -742,7 +730,7 @@ If the box is colored green, then text matches the individual author's sort stri ... - + :/images/book.png:/images/book.png @@ -762,7 +750,7 @@ If the box is colored green, then text matches the individual author's sort stri - + :/images/edit_input.png:/images/edit_input.png @@ -863,6 +851,11 @@ If the box is colored green, then text matches the individual author's sort stri
calibre/gui2/comments_editor.h
1 + + CompleteComboBox + QComboBox +
widgets.h
+
title diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py index 4f72fa915e..95c7cf9225 100644 --- a/src/calibre/gui2/dialogs/search.py +++ b/src/calibre/gui2/dialogs/search.py @@ -31,6 +31,9 @@ class SearchDialog(QDialog, Ui_Dialog): self.authors_box.setEditText('') self.authors_box.completer().setCompletionMode(QCompleter.PopupCompletion) self.authors_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive) + self.authors_box.set_separator('&') + self.authors_box.set_space_before_sep(True) + self.authors_box.update_items_cache(self.db.all_author_names()) all_series = db.all_series() all_series.sort(key=lambda x : sort_key(x[1])) diff --git a/src/calibre/gui2/dialogs/search.ui b/src/calibre/gui2/dialogs/search.ui index 519ae8c462..06ea9de379 100644 --- a/src/calibre/gui2/dialogs/search.ui +++ b/src/calibre/gui2/dialogs/search.ui @@ -6,15 +6,15 @@ 0 0 - 731 - 411 + 752 + 472
Advanced Search - + :/images/search.png:/images/search.png @@ -265,7 +265,7 @@
- + Enter an author's name. Only one author can be used. @@ -364,6 +364,11 @@ QLineEdit
widgets.h
+ + CompleteComboBox + QComboBox +
widgets.h
+
all diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 6e2d52f835..0bb5ee7634 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -540,6 +540,22 @@ class EnComboBox(QComboBox): idx = 0 self.setCurrentIndex(idx) +class CompleteComboBox(EnComboBox): + + def __init__(self, *args): + EnComboBox.__init__(self, *args) + self.setLineEdit(CompleteLineEdit(self)) + + def update_items_cache(self, complete_items): + self.lineEdit().update_items_cache(complete_items) + + def set_separator(self, sep): + self.lineEdit().set_separator(sep) + + def set_space_before_sep(self, space_before): + self.lineEdit().set_space_before_sep(space_before) + + class HistoryLineEdit(QComboBox): lost_focus = pyqtSignal() From ca0e545253e3d4b5a6bf641cac78e9b816f10d81 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Jan 2011 16:51:26 -0700 Subject: [PATCH 15/36] E-book viewer: Display cover when viewing FB2 files --- src/calibre/ebooks/fb2/input.py | 18 +++++++++++------- src/calibre/ebooks/oeb/iterator.py | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/calibre/ebooks/fb2/input.py b/src/calibre/ebooks/fb2/input.py index b019873d39..3d3ec69833 100644 --- a/src/calibre/ebooks/fb2/input.py +++ b/src/calibre/ebooks/fb2/input.py @@ -104,13 +104,17 @@ class FB2Input(InputFormatPlugin): entries = [(f, guess_type(f)[0]) for f in os.listdir('.')] opf.create_manifest(entries) opf.create_spine(['index.xhtml']) - - for img in doc.xpath('//f:coverpage/f:image', namespaces=NAMESPACES): - href = img.get('{%s}href'%XLINK_NS, img.get('href', None)) - if href is not None: - if href.startswith('#'): - href = href[1:] - opf.guide.set_cover(os.path.abspath(href)) + if mi.cover_data and mi.cover_data[1]: + with open('fb2_cover_calibre_mi.jpg', 'wb') as f: + f.write(mi.cover_data[1]) + opf.guide.set_cover(os.path.abspath('fb2_cover_calibre_mi.jpg')) + else: + for img in doc.xpath('//f:coverpage/f:image', namespaces=NAMESPACES): + href = img.get('{%s}href'%XLINK_NS, img.get('href', None)) + if href is not None: + if href.startswith('#'): + href = href[1:] + opf.guide.set_cover(os.path.abspath(href)) opf.render(open('metadata.opf', 'wb')) return os.path.join(os.getcwd(), 'metadata.opf') diff --git a/src/calibre/ebooks/oeb/iterator.py b/src/calibre/ebooks/oeb/iterator.py index 6820709b3e..08b4369078 100644 --- a/src/calibre/ebooks/oeb/iterator.py +++ b/src/calibre/ebooks/oeb/iterator.py @@ -227,7 +227,7 @@ class EbookIterator(object): self.log.warn('Missing spine item:', repr(spath)) cover = self.opf.cover - if self.ebook_ext in ('lit', 'mobi', 'prc', 'opf') and cover: + if self.ebook_ext in ('lit', 'mobi', 'prc', 'opf', 'fb2') and cover: cfile = os.path.join(self.base, 'calibre_iterator_cover.html') chtml = (TITLEPAGE%os.path.relpath(cover, self.base).replace(os.sep, '/')).encode('utf-8') From 7a79f8d98d2556803177769797e5b51e169e48a1 Mon Sep 17 00:00:00 2001 From: Shixin Zeng Date: Sun, 16 Jan 2011 19:43:13 -0600 Subject: [PATCH 16/36] make use_embedded_content settable per feed --- src/calibre/web/feeds/news.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index ee5b11c5f6..dd32d3749f 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -901,10 +901,7 @@ class BasicNewsRecipe(Recipe): if self.test: feeds = feeds[:2] self.has_single_feed = len(feeds) == 1 - - if self.use_embedded_content is None: - self.use_embedded_content = feeds[0].has_embedded_content() - + index = os.path.join(self.output_dir, 'index.html') html = self.feeds2index(feeds) @@ -939,7 +936,9 @@ class BasicNewsRecipe(Recipe): url = None if not url: continue - func, arg = (self.fetch_embedded_article, article) if self.use_embedded_content else \ + func, arg = (self.fetch_embedded_article, article) \ + if self.use_embedded_content or (self.use_embedded_content == None and feed.has_embedded_content()) \ + else \ ((self.fetch_obfuscated_article if self.articles_are_obfuscated \ else self.fetch_article), url) req = WorkRequest(func, (arg, art_dir, f, a, len(feed)), From 34da8b73ccf55b846c6e612f923682f1abe3f09c Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 16 Jan 2011 21:35:53 -0500 Subject: [PATCH 17/36] Fix bug #8381: reference \t and \T PML indents properly. --- src/calibre/ebooks/pml/pmlconverter.py | 35 ++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/calibre/ebooks/pml/pmlconverter.py b/src/calibre/ebooks/pml/pmlconverter.py index 10e5871d31..a0814ee0dd 100644 --- a/src/calibre/ebooks/pml/pmlconverter.py +++ b/src/calibre/ebooks/pml/pmlconverter.py @@ -34,18 +34,15 @@ class PML_HTMLizer(object): 'ra', 'c', 'r', - 't', 's', 'l', 'k', - 'T', 'FN', 'SB', ] STATES_VALUE_REQ = [ 'a', - 'T', 'FN', 'SB', ] @@ -96,8 +93,6 @@ class PML_HTMLizer(object): 'Sb': 'sb', 'c': 'c', 'r': 'r', - 't': 't', - 'T': 'T', 'i': 'i', 'I': 'i', 'u': 'u', @@ -133,8 +128,6 @@ class PML_HTMLizer(object): DIV_STATES = [ 'c', 'r', - 't', - 'T', 'FN', 'SB', ] @@ -255,8 +248,6 @@ class PML_HTMLizer(object): for key, val in self.state.items(): if val[0]: - if key == 'T': - self.state['T'][0] = False if key in self.DIV_STATES: div.append(key) elif key in self.SPAN_STATES: @@ -506,6 +497,9 @@ class PML_HTMLizer(object): self.toc = TOC() self.file_name = file_name + indent_state = {'t': False, 'T': False} + adv_indent_val = '' + for s in self.STATES: self.state[s] = [False, '']; @@ -515,6 +509,8 @@ class PML_HTMLizer(object): parsed = [] empty = True + basic_indent = indent_state['t'] + adv_indent = indent_state['T'] # Must use StringIO, cStringIO does not support unicode line = StringIO.StringIO(line) @@ -527,7 +523,7 @@ class PML_HTMLizer(object): if c == '\\': c = line.read(1) - if c in 'qcrtTiIuobBlk': + if c in 'qcriIuobBlk': text = self.process_code(c, line) elif c in 'FS': l = line.read(1) @@ -574,6 +570,15 @@ class PML_HTMLizer(object): elif c == 'w': empty = False text = '
' % self.code_value(line) + elif c == 't': + indent_state[c] = not indent_state[c] + if indent_state[c]: + basic_indent = True + elif c == 'T': + indent_state[c] = not indent_state[c] + if indent_state[c]: + adv_indent = True + adv_indent_val = self.code_value(line) elif c == '-': empty = False text = '­' @@ -590,6 +595,16 @@ class PML_HTMLizer(object): if not empty: text = self.end_line() parsed.append(text) + + if basic_indent: + parsed.insert(0, self.STATES_TAGS['t'][0]) + parsed.append(self.STATES_TAGS['t'][1]) + elif adv_indent: + parsed.insert(0, self.STATES_TAGS['T'][0] % adv_indent_val) + parsed.append(self.STATES_TAGS['T'][1]) + indent_state['T'] = False + adv_indent_val = '' + output.append(u''.join(parsed)) line.close() From 899263f3b34febbe3f7c1fc435017b14b502a802 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Jan 2011 20:37:04 -0700 Subject: [PATCH 18/36] Updated Nature News --- resources/recipes/freenature.recipe | 66 +++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/resources/recipes/freenature.recipe b/resources/recipes/freenature.recipe index cf06e7163d..0b287842ec 100644 --- a/resources/recipes/freenature.recipe +++ b/resources/recipes/freenature.recipe @@ -1,4 +1,5 @@ from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ebooks.BeautifulSoup import Tag import re class NatureNews(BasicNewsRecipe): @@ -10,17 +11,76 @@ class NatureNews(BasicNewsRecipe): max_articles_per_feed = 50 no_stylesheets = True - remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'}) - remove_tags_after = dict(name='h2', attrs={'id':'comments'}) + keep_only_tags = [dict(name='div', attrs={'id':'content'})] +# remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'}) +# remove_tags_after = dict(name='h2', attrs={'id':'comments'}) remove_tags = [ dict(name='h2', attrs={'id':'comments'}), dict(attrs={'alt':'Advertisement'}), dict(name='div', attrs={'class':'ad'}), - ] + dict(attrs={'class':'Z3988'}), + dict(attrs={'class':['formatpublished','type-of-article','cleardiv','disclaimer','buttons','comments xoxo']}), + dict(name='a', attrs={'href':'#comments'}), + dict(name='h2',attrs={'class':'subheading plusicon icon-add-comment'}) + ] preprocess_regexps = [ (re.compile(r'

ADVERTISEMENT

', re.DOTALL|re.IGNORECASE), lambda match: ''), ] + extra_css = ''' + .author { text-align: right; font-size: small; line-height:1em; margin-top:0px; margin-left:0; margin-right:0; margin-bottom: 0; } + .imagedescription { font-size: small; font-style:italic; line-height:1em; margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; } + .imagecredit { font-size: x-small; font-style: normal; font-weight: bold} + ''' + feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')] + def preprocess_html(self,soup): + # The author name is slightly buried - dig it up + author = soup.find('p', {'class':'byline'}) + if author: + # Find out the author's name + authornamediv = author.find('span',{'class':'author fn'}) + authornamelink = authornamediv.find('a') + if authornamelink: + authorname = authornamelink.contents[0] + else: + authorname = authornamediv.contents[0] + # Stick the author's name in the byline tag + tag = Tag(soup,'div') + tag['class'] = 'author' + tag.insert(0,authorname.strip()) + author.replaceWith(tag) + + # Change the intro from a p to a div + intro = soup.find('p',{'class':'intro'}) + if intro: + tag = Tag(soup,'div') + tag['class'] = 'intro' + tag.insert(0,intro.contents[0]) + intro.replaceWith(tag) + + # Change span class=imagedescription to div + descr = soup.find('span',{'class':'imagedescription'}) + if descr: + tag = Tag(soup,'div') + tag['class'] = 'imagedescription' + tag.insert(0,descr.renderContents()) + descr.replaceWith(tag) + + # The references are in a list, let's make them simpler + reflistcont = soup.find('ul',{'id':'article-refrences'}) + if reflistcont: + reflist = reflistcont.li.renderContents() + tag = Tag(soup,'div') + tag['class'] = 'article-references' + tag.insert(0,reflist) + reflistcont.replaceWith(tag) + + # Within the id=content div, we need to remove all the stuff after the end of the class=entry-content + entrycontent = soup.find('div',{'class':'entry-content'}) + for nextSibling in entrycontent.findNextSiblings(): + nextSibling.extract() + + return soup From a661a17f1f027ab2cbf9b006e85a2a366d4e4c37 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 17 Jan 2011 11:27:10 +0000 Subject: [PATCH 19/36] Change formatter to show an error if an unknown function is used. --- src/calibre/utils/formatter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 49b807ff1c..740e67bee8 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -274,9 +274,9 @@ class TemplateFormatter(string.Formatter): colon += 1 funcs = formatter_functions.get_functions() - if fmt[colon:p] in funcs: - field = fmt[colon:p] - func = funcs[field] + fname = fmt[colon:p] + if fname in funcs: + func = funcs[fname] if func.arg_count == 2: # only one arg expected. Don't bother to scan. Avoids need # for escaping characters @@ -292,6 +292,8 @@ class TemplateFormatter(string.Formatter): else: val = func.eval_(self, self.kwargs, self.book, self.locals, val, *args).strip() + else: + return _('%s: unknown function')%fname if val: val = self._do_format(val, dispfmt) if not val: From 20d8d908ee70d69c68bba1bb221e8e850fb51da7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 17 Jan 2011 08:37:07 -0700 Subject: [PATCH 20/36] ... --- src/calibre/devices/sne/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/sne/driver.py b/src/calibre/devices/sne/driver.py index 04e5cd0d76..bb8d34c59c 100644 --- a/src/calibre/devices/sne/driver.py +++ b/src/calibre/devices/sne/driver.py @@ -33,6 +33,6 @@ class SNE(USBMS): STORAGE_CARD_VOLUME_LABEL = 'SNE Storage Card' EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Books' - SUPPORTS_SUB_DIRS = False + SUPPORTS_SUB_DIRS = True From 9013a5d97dc73e6f3ea416b6b30a2cdf3522fcc0 Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 17 Jan 2011 08:53:18 -0700 Subject: [PATCH 21/36] GwR catalog 1.0 revisions --- resources/catalog/section_list_templates.py | 39 ++ src/calibre/gui2/actions/catalog.py | 18 +- src/calibre/library/catalog.py | 550 ++++++++------------ 3 files changed, 279 insertions(+), 328 deletions(-) create mode 100644 resources/catalog/section_list_templates.py diff --git a/resources/catalog/section_list_templates.py b/resources/catalog/section_list_templates.py new file mode 100644 index 0000000000..de73147fcf --- /dev/null +++ b/resources/catalog/section_list_templates.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +''' + Available fields: + {title} Title of the book + {series} Series name + {series_index} Number of the book in the series + {rating} Rating + {rating_parens} Rating, in parentheses + {pubyear} Year the book was published + {pubyear_parens} Year the book was published, in parentheses +''' +# Books by Author +by_authors_normal_title_template = '{title} {pubyear_parens}' +by_authors_series_title_template = '[{series_index}] {title} {pubyear_parens}' + +# Books by Title +by_titles_normal_title_template = '{title}' +by_titles_series_title_template = '{title} ({series} [{series_index}])' + +# Books by Series +by_series_title_template = '[{series_index}] {title} {pubyear_parens}' + +# Books by Genre +by_genres_normal_title_template = '{title} {pubyear_parens}' +by_genres_series_title_template = '{series_index}. {title} {pubyear_parens}' + +# Recently Added +by_recently_added_normal_title_template = '{title}' +by_recently_added_series_title_template = '{title} ({series} [{series_index}])' + +# By Month added +by_month_added_normal_title_template = '{title} {pubyear_parens}' +by_month_added_series_title_template = '[{series_index}] {title} {pubyear_parens}' \ No newline at end of file diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py index 6d3bb539a2..1650c80d70 100644 --- a/src/calibre/gui2/actions/catalog.py +++ b/src/calibre/gui2/actions/catalog.py @@ -5,11 +5,11 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, shutil +import re, os, shutil from PyQt4.Qt import QModelIndex -from calibre.gui2 import error_dialog, choose_dir +from calibre.gui2 import choose_dir, error_dialog, info_dialog, warning_dialog from calibre.gui2.tools import generate_catalog from calibre.utils.config import dynamic from calibre.gui2.actions import InterfaceAction @@ -55,10 +55,16 @@ class GenerateCatalogAction(InterfaceAction): def catalog_generated(self, job): if job.result: - # Error during catalog generation - return error_dialog(self.gui, _('Catalog generation terminated'), - job.result, - show=True) + # Problems during catalog generation + dialog_title = job.result.pop(0) + if re.match('warning:', job.result[0].lower()): + job.result.append("Catalog generation complete.") + warning_dialog(self.gui, dialog_title, '\n'.join(job.result), show=True) + else: + job.result.append("Catalog generation terminated.") + error_dialog(self.gui, dialog_title,'\n'.join(job.result),show=True) + return + if job.failed: return self.gui.job_exception(job) id = self.gui.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 087d40c4eb..f1c5e3ae65 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -546,9 +546,9 @@ class EPUB_MOBI(CatalogPlugin): name = 'Catalog_EPUB_MOBI' description = 'EPUB/MOBI catalog generator' supported_platforms = ['windows', 'osx', 'linux'] - minimum_calibre_version = (0, 6, 34) + minimum_calibre_version = (0, 7, 40) author = 'Greg Riker' - version = (0, 0, 1) + version = (1, 0, 0) file_types = set(['epub','mobi']) THUMB_SMALLEST = "1.0" @@ -900,15 +900,7 @@ class EPUB_MOBI(CatalogPlugin): ''' Generates catalog source files from calibre database - Implementation notes - - 'Marker tags' in a book's metadata are used to flag special conditions: - (Defaults) - '~' : Do not catalog this book - '+' : Mark this book as read (check mark) in lists - '*' : Display trailing text as 'Note: ' in top frame next to cover - '[] : Source of content (e.g., Amazon, Project Gutenberg). Do not create genre - - - Program flow + Flow of control: gui2.actions.catalog:generate_catalog() gui2.tools:generate_catalog() or library.cli:command_catalog() called from gui2.convert.gui_conversion:gui_catalog() @@ -953,7 +945,7 @@ class EPUB_MOBI(CatalogPlugin): self.__creator = opts.creator self.__db = db self.__descriptionClip = opts.descriptionClip - self.__error = None + self.__error = [] self.__generateForKindle = True if (self.opts.fmt == 'mobi' and \ self.opts.output_profile and \ self.opts.output_profile.startswith("kindle")) else False @@ -1033,6 +1025,22 @@ class EPUB_MOBI(CatalogPlugin): # +1 thumbs self.__totalSteps += 3 + # Load section list templates + templates = ['by_authors_normal_title_template', + 'by_authors_series_title_template', + 'by_titles_normal_title_template', + 'by_titles_series_title_template', + 'by_series_title_template', + 'by_genres_normal_title_template', + 'by_genres_series_title_template', + 'by_recently_added_normal_title_template', + 'by_recently_added_series_title_template', + 'by_month_added_normal_title_template', + 'by_month_added_series_title_template'] + execfile(P(os.path.join('catalog','section_list_templates.py')),locals()) + for t in templates: + setattr(self,t,eval(t)) + # Accessors if True: ''' @@ -1420,26 +1428,12 @@ class EPUB_MOBI(CatalogPlugin): ''' self.updateProgressFullStep("Sorting database") - - ''' - # Sort titles case-insensitive, by author - self.booksByAuthor = sorted(self.booksByTitle, - key=lambda x:(x['author_sort'].upper(), x['author_sort'].upper())) - ''' - self.booksByAuthor = list(self.booksByTitle) - self.booksByAuthor.sort(self.author_compare) - - if False and self.verbose: - self.opts.log.info("fetchBooksByAuthor(): %d books" % len(self.booksByAuthor)) - self.opts.log.info(" %-30s %-20s %s" % ('title', 'series', 'series_index')) - for title in self.booksByAuthor: - self.opts.log.info((u" %-30s %-20s%5s " % \ - (title['title'][:30], - title['series'][:20] if title['series'] else '', - title['series_index'], - )).encode('utf-8')) - raise SystemExit + self.booksByAuthor = sorted(self.booksByAuthor, key=self.booksByAuthorSorter_author) +# for book in self.booksByAuthor: +# print "{0:<30} {1:<30} {2:<30}".format(book['title'],book['author'],book['author_sort']) +# print +# stop # Build the unique_authors set from existing data authors = [(record['author'], record['author_sort'].capitalize()) for record in self.booksByAuthor] @@ -1457,16 +1451,17 @@ class EPUB_MOBI(CatalogPlugin): multiple_authors = True if author != current_author and i: - # Warn, exit if friendly matches previous, but sort doesn't + # Exit if author matches previous, but author_sort doesn't match if author[0] == current_author[0]: error_msg = _(''' -\n*** Metadata error *** -Inconsistent Author Sort values for Author '{0}', unable to continue building catalog. +Inconsistent Author Sort values for Author '{0}', unable to continue building catalog.\n Select all books by '{0}', apply correct Author Sort value in Edit Metadata dialog, then rebuild the catalog.\n''').format(author[0]) - + self.opts.log.warn('\n*** Metadata error ***') self.opts.log.warn(error_msg) - self.error = error_msg + + self.error.append('Metadata error') + self.error.append(error_msg) return False # New author, save the previous author/sort/count @@ -1496,16 +1491,8 @@ then rebuild the catalog.\n''').format(author[0]) return True def fetchBooksByTitle(self): - self.updateProgressFullStep("Fetching database") - # Get the database as a dictionary - # Sort by title - # Search is a string like this: - # not tag: author:"Riker" - # So we need to merge opts.exclude_tag with opts.search_text - # not tag:"~" author:"Riker" - self.opts.sort_by = 'title' # Merge opts.exclude_tags with opts.search_text @@ -1528,7 +1515,6 @@ then rebuild the catalog.\n''').format(author[0]) else: self.opts.search_text = search_phrase - #print "fetchBooksByTitle(): opts.search_text: %s" % self.opts.search_text # Fetch the database as a dictionary data = self.plugin.search_sort_db(self.db, self.opts) data = self.processExclusions(data) @@ -1536,8 +1522,6 @@ then rebuild the catalog.\n''').format(author[0]) # Populate this_title{} from data[{},{}] titles = [] for record in data: - if False: - print "available record metadata:\n%s" % sorted(record.keys()) this_title = {} this_title['id'] = record['id'] @@ -1547,7 +1531,6 @@ then rebuild the catalog.\n''').format(author[0]) if record['series']: this_title['series'] = record['series'] this_title['series_index'] = record['series_index'] - this_title['title'] = self.generateSeriesTitle(this_title) else: this_title['series'] = None this_title['series_index'] = 0.0 @@ -1572,7 +1555,12 @@ then rebuild the catalog.\n''').format(author[0]) this_title['publisher'] = re.sub('&', '&', record['publisher']) this_title['rating'] = record['rating'] if record['rating'] else 0 - this_title['date'] = strftime(u'%B %Y', record['pubdate'].timetuple()) + + if re.match('0100-01-01',str(record['pubdate'].date())): + this_title['date'] = None + else: + this_title['date'] = strftime(u'%B %Y', record['pubdate'].timetuple()) + this_title['timestamp'] = record['timestamp'] if record['comments']: @@ -1646,7 +1634,7 @@ then rebuild the catalog.\n''').format(author[0]) title['title_sort'][0:40])).decode('mac-roman')) return True else: - self.error = _("No books found to catalog.\nCheck 'Excluded books' criteria in E-book options.") + self.error.append( _("No books found to catalog.\nCheck 'Excluded books' criteria in E-book options.")) return False def fetchBookmarks(self): @@ -1748,13 +1736,12 @@ then rebuild the catalog.\n''').format(author[0]) self.bookmarked_books = {} def generateHTMLDescriptions(self): - # Write each title to a separate HTML file in contentdir + ''' + Write each title to a separate HTML file in contentdir + ''' self.updateProgressFullStep("'Descriptions'") for (title_num, title) in enumerate(self.booksByTitle): - if False: - self.opts.log.info("%3s: %s - %s" % (title['id'], title['title'], title['author'])) - self.updateProgressMicroStep("Description %d of %d" % \ (title_num, len(self.booksByTitle)), float(title_num*100/len(self.booksByTitle))/100) @@ -1768,8 +1755,9 @@ then rebuild the catalog.\n''').format(author[0]) outfile.close() def generateHTMLByTitle(self): - # Write books by title A-Z to HTML file - + ''' + Write books by title A-Z to HTML file + ''' self.updateProgressFullStep("'Titles'") soup = self.generateHTMLEmptyHeader("Books By Alpha Title") @@ -1807,22 +1795,11 @@ then rebuild the catalog.\n''').format(author[0]) current_letter = "" # Re-sort title list without leading series/series_index + # Incoming title : if not self.useSeriesPrefixInTitlesSection: nspt = deepcopy(self.booksByTitle) - for book in nspt: - if book['series']: - tokens = book['title'].partition(':') - book['title'] = '%s (%s)' % (tokens[2].strip(), tokens[0]) - book['title_sort'] = self.generateSortTitle(book['title']) - nspt = sorted(nspt, - key=lambda x:(x['title_sort'].upper(), x['title_sort'].upper())) + nspt = sorted(nspt, key=lambda x:(x['title_sort'].upper(), x['title_sort'].upper())) self.booksByTitle_noSeriesPrefix = nspt - if False and self.verbose: - self.opts.log.info("no_series_prefix_titles: %d books" % len(nspt)) - self.opts.log.info(" %-40s %-40s" % ('title', 'title_sort')) - for title in nspt: - self.opts.log.info((u" %-40s %-40s" % (title['title'][0:40], - title['title_sort'][0:40])).encode('utf-8')) # Loop through the books by title title_list = self.booksByTitle @@ -1878,7 +1855,14 @@ then rebuild the catalog.\n''').format(author[0]) aTag = Tag(soup, "a") if self.opts.generate_descriptions: aTag['href'] = "book_%d.html" % (int(float(book['id']))) - aTag.insert(0,escape(book['title'])) + + # Generate the title from the template + args = self.generateFormatArgs(book) + if book['series']: + formatted_title = self.by_titles_series_title_template.format(**args).rstrip() + else: + formatted_title = self.by_titles_normal_title_template.format(**args).rstrip() + aTag.insert(0,NavigableString(escape(formatted_title))) pBookTag.insert(ptc, aTag) ptc += 1 @@ -1916,7 +1900,9 @@ then rebuild the catalog.\n''').format(author[0]) self.htmlFileList_1.append("content/ByAlphaTitle.html") def generateHTMLByAuthor(self): - # Write books by author A-Z + ''' + Write books by author A-Z + ''' self.updateProgressFullStep("'Authors'") friendly_name = "Authors" @@ -1953,7 +1939,8 @@ then rebuild the catalog.\n''').format(author[0]) current_author = '' current_letter = '' current_series = None - for book in self.booksByAuthor: + for book in sorted(self.booksByAuthor, key = self.booksByAuthorSorter_author_sort): + book_count += 1 if self.letter_or_symbol(book['author_sort'][0].upper()) != current_letter : # Start a new letter with Index letter @@ -2067,14 +2054,18 @@ then rebuild the catalog.\n''').format(author[0]) aTag = Tag(soup, "a") if self.opts.generate_descriptions: aTag['href'] = "book_%d.html" % (int(float(book['id']))) - # Use series, series index if avail else title, + year of publication + + # Generate the title from the template + args = self.generateFormatArgs(book) if current_series: - aTag.insert(0,'%s (%s)' % (escape(book['title'][len(book['series'])+1:]), - book['date'].split()[1])) + #aTag.insert(0,'%s%s' % (escape(book['title'][len(book['series'])+1:]),pubyear)) + formatted_title = self.by_authors_series_title_template.format(**args).rstrip() else: - aTag.insert(0,'%s (%s)' % (escape(book['title']), - book['date'].split()[1])) + #aTag.insert(0,'%s%s' % (escape(book['title']), pubyear)) + formatted_title = self.by_authors_normal_title_template.format(**args).rstrip() non_series_books += 1 + aTag.insert(0,NavigableString(escape(formatted_title))) + pBookTag.insert(ptc, aTag) ptc += 1 @@ -2111,7 +2102,6 @@ then rebuild the catalog.\n''').format(author[0]) # Add the divTag to the body body.insert(btc, divTag) - # Write the generated file to contentdir outfile_spec = "%s/ByAlphaAuthor.html" % (self.contentDir) outfile = open(outfile_spec, 'w') @@ -2120,13 +2110,15 @@ then rebuild the catalog.\n''').format(author[0]) self.htmlFileList_1.append("content/ByAlphaAuthor.html") def generateHTMLByDateAdded(self): - # Write books by reverse chronological order + ''' + Write books by reverse chronological order + ''' self.updateProgressFullStep("'Recently Added'") def add_books_to_HTML_by_month(this_months_list, dtc): if len(this_months_list): - this_months_list.sort(self.author_compare) + this_months_list = sorted(this_months_list, key=self.booksByAuthorSorter_author_sort) # Create a new month anchor date_string = strftime(u'%B %Y', current_date.timetuple()) @@ -2156,16 +2148,6 @@ then rebuild the catalog.\n''').format(author[0]) divTag.insert(dtc,pAuthorTag) dtc += 1 - ''' - # Insert an <hr /> between non-series and series - if not current_series and non_series_books and new_entry['series']: - # Insert an <hr /> - hrTag = Tag(soup,'hr') - hrTag['class'] = "series_divider" - divTag.insert(dtc,hrTag) - dtc += 1 - ''' - # Check for series if new_entry['series'] and new_entry['series'] != current_series: # Start a new series @@ -2213,11 +2195,15 @@ then rebuild the catalog.\n''').format(author[0]) aTag = Tag(soup, "a") if self.opts.generate_descriptions: aTag['href'] = "book_%d.html" % (int(float(new_entry['id']))) + + # Generate the title from the template + args = self.generateFormatArgs(new_entry) if current_series: - aTag.insert(0,escape(new_entry['title'][len(new_entry['series'])+1:])) + formatted_title = self.by_month_added_series_title_template.format(**args).rstrip() else: - aTag.insert(0,escape(new_entry['title'])) + formatted_title = self.by_month_added_normal_title_template.format(**args).rstrip() non_series_books += 1 + aTag.insert(0,NavigableString(escape(formatted_title))) pBookTag.insert(ptc, aTag) ptc += 1 @@ -2265,7 +2251,14 @@ then rebuild the catalog.\n''').format(author[0]) aTag = Tag(soup, "a") if self.opts.generate_descriptions: aTag['href'] = "book_%d.html" % (int(float(new_entry['id']))) - aTag.insert(0,escape(new_entry['title'])) + + # Generate the title from the template + args = self.generateFormatArgs(new_entry) + if new_entry['series']: + formatted_title = self.by_recently_added_series_title_template.format(**args).rstrip() + else: + formatted_title = self.by_recently_added_normal_title_template.format(**args).rstrip() + aTag.insert(0,NavigableString(escape(formatted_title))) pBookTag.insert(ptc, aTag) ptc += 1 @@ -2323,17 +2316,12 @@ then rebuild the catalog.\n''').format(author[0]) divTag = Tag(soup, "div") dtc = 0 - # Add books by date range + # >>> Books by date range <<< if self.useSeriesPrefixInTitlesSection: self.booksByDateRange = sorted(self.booksByTitle, key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) else: nspt = deepcopy(self.booksByTitle) - for book in nspt: - if book['series']: - tokens = book['title'].partition(':') - book['title'] = '%s (%s)' % (tokens[2].strip(), tokens[0]) - book['title_sort'] = self.generateSortTitle(book['title']) self.booksByDateRange = sorted(nspt, key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) date_range_list = [] @@ -2356,15 +2344,6 @@ then rebuild the catalog.\n''').format(author[0]) dtc = add_books_to_HTML_by_date_range(date_range_list, date_range, dtc) date_range_list = [book] - ''' - if books_added_in_date_range: - # Add an <hr> separating date ranges from months - hrTag = Tag(soup,'hr') - hrTag['class'] = "description_divider" - divTag.insert(dtc,hrTag) - dtc += 1 - ''' - # >>>> Books by month <<<< # Sort titles case-insensitive for by month using series prefix self.booksByMonth = sorted(self.booksByTitle, @@ -2395,7 +2374,9 @@ then rebuild the catalog.\n''').format(author[0]) self.htmlFileList_2.append("content/ByDateAdded.html") def generateHTMLByDateRead(self): - # Write books by active bookmarks + ''' + Write books by active bookmarks + ''' friendly_name = 'Recently Read' self.updateProgressFullStep("'%s'" % friendly_name) if not self.bookmarked_books: @@ -2533,32 +2514,6 @@ then rebuild the catalog.\n''').format(author[0]) self.booksByDateRead = sorted(bookmarked_books, key=lambda x:(x['bookmark_timestamp'], x['bookmark_timestamp']),reverse=True) - ''' - # >>>> Recently by date range <<<< - date_range_list = [] - today_time = datetime.datetime.utcnow() - today_time.replace(hour=23, minute=59, second=59) - books_added_in_date_range = False - for (i, date) in enumerate(self.DATE_RANGE): - date_range_limit = self.DATE_RANGE[i] - if i: - date_range = '%d to %d days ago' % (self.DATE_RANGE[i-1], self.DATE_RANGE[i]) - else: - date_range = 'Last %d days' % (self.DATE_RANGE[i]) - - for book in self.booksByDateRead: - bookmark_time = datetime.datetime.utcfromtimestamp(book['bookmark_timestamp']) - delta = today_time-bookmark_time - if delta.days <= date_range_limit: - date_range_list.append(book) - books_added_in_date_range = True - else: - break - - dtc = add_books_to_HTML_by_date_range(date_range_list, date_range, dtc) - date_range_list = [book] - ''' - # >>>> Recently read by day <<<< current_date = datetime.date.fromordinal(1) todays_list = [] @@ -2713,10 +2668,15 @@ then rebuild the catalog.\n''').format(author[0]) # Use series, series index if avail else just title #aTag.insert(0,'%d. %s · %s' % (book['series_index'],escape(book['title']), ' & '.join(book['authors']))) - # Link to book - aTag.insert(0,'%d. %s (%s)' % (book['series_index'], - escape(book['title']), - strftime(u'%Y', book['pubdate'].timetuple()))) + # Reassert 'date' since this is the result of a new search + if re.match('0100-01-01',str(book['pubdate'].date())): + book['date'] = None + else: + book['date'] = strftime(u'%B %Y', book['pubdate'].timetuple()) + + args = self.generateFormatArgs(book) + formatted_title = self.by_series_title_template.format(**args).rstrip() + aTag.insert(0,NavigableString(escape(formatted_title))) pBookTag.insert(ptc, aTag) ptc += 1 @@ -2760,10 +2720,11 @@ then rebuild the catalog.\n''').format(author[0]) self.htmlFileList_1.append("content/BySeries.html") def generateHTMLByTags(self): - # Generate individual HTML files for each tag, e.g. Fiction, Nonfiction ... - # Note that special tags - ~+*[] - have already been filtered from books[] - # There may be synonomous tags - + ''' + Generate individual HTML files for each tag, e.g. Fiction, Nonfiction ... + Note that special tags - have already been filtered from books[] + There may be synonomous tags + ''' self.updateProgressFullStep("'Genres'") self.genre_tags_dict = self.filterDbTags(self.db.all_tags()) @@ -2787,6 +2748,8 @@ then rebuild the catalog.\n''').format(author[0]) this_book['tags'] = book['tags'] this_book['id'] = book['id'] this_book['series'] = book['series'] + this_book['series_index'] = book['series_index'] + this_book['date'] = book['date'] normalized_tag = self.genre_tags_dict[friendly_tag] genre_tag_list = [key for genre in genre_list for key in genre] if normalized_tag in genre_tag_list: @@ -2843,13 +2806,7 @@ then rebuild the catalog.\n''').format(author[0]) unique_authors.append((current_author[0], current_author[1], books_by_current_author)) else: books_by_current_author += 1 - ''' - # Extract the unique entries - unique_authors = [] - for author in authors: - if not author in unique_authors: - unique_authors.append(author) - ''' + # Write the genre book list as an article titles_spanned = self.generateHTMLByGenre(genre, True if index==0 else False, genre_tag_set[genre], @@ -2863,18 +2820,14 @@ then rebuild the catalog.\n''').format(author[0]) 'books':genre_tag_set[genre], 'titles_spanned':titles_spanned}) - if False and self.opts.verbose: - for genre in master_genre_list: - print "genre['tag']: %s" % genre['tag'] - for book in genre['books']: - print book['title'] self.genres = master_genre_list def generateThumbnails(self): - # Generate a thumbnail per cover. If a current thumbnail exists, skip - # If a cover doesn't exist, use default - # Return list of active thumbs - + ''' + Generate a thumbnail per cover. If a current thumbnail exists, skip + If a cover doesn't exist, use default + Return list of active thumbs + ''' self.updateProgressFullStep("'Thumbnails'") thumbs = ['thumbnail_default.jpg'] image_dir = "%s/images" % self.catalogPath @@ -2886,45 +2839,51 @@ then rebuild the catalog.\n''').format(author[0]) thumb_file = 'thumbnail_%d.jpg' % int(title['id']) thumb_generated = True + valid_cover = True try: - self.generateThumbnail(title, image_dir, thumb_file) thumbs.append("thumbnail_%d.jpg" % int(title['id'])) + self.generateThumbnail(title, image_dir, thumb_file) except: + if 'cover' in title and os.path.exists(title['cover']): + valid_cover = False + self.opts.log.warn(" *** Invalid cover file for '%s' ***" % (title['title'])) + if not self.error: + self.error.append('Invalid cover files') + self.error.append("Warning: invalid cover file for '%s', default cover substituted.\n" % (title['title'])) thumb_generated = False - if not thumb_generated: - # Use default cover - if False and self.verbose: - self.opts.log.warn(" using default cover for '%s'" % \ - (title['title'])) + self.opts.log.warn(" using default cover for '%s'" % (title['title'])) # Check to make sure default is current # Check to see if thumbnail exists - thumb_fp = "%s/thumbnail_default.jpg" % (image_dir) - cover = "%s/DefaultCover.png" % (self.catalogPath) + default_thumb_fp = os.path.join(image_dir,"thumbnail_default.jpg") + cover = os.path.join(self.catalogPath, "DefaultCover.png") if not os.path.exists(cover): shutil.copyfile(I('book.png'), cover) - if os.path.isfile(thumb_fp): + if os.path.isfile(default_thumb_fp): # Check to see if default cover is newer than thumbnail # os.path.getmtime() = modified time # os.path.ctime() = creation time cover_timestamp = os.path.getmtime(cover) - thumb_timestamp = os.path.getmtime(thumb_fp) + thumb_timestamp = os.path.getmtime(default_thumb_fp) if thumb_timestamp < cover_timestamp: if False and self.verbose: self.opts.log.warn("updating thumbnail_default for %s" % title['title']) - #title['cover'] = "%s/DefaultCover.jpg" % self.catalogPath + #title['cover'] = os.path.join(self.catalogPath,"DefaultCover.jpg") title['cover'] = cover - self.generateThumbnail(title, image_dir, "thumbnail_default.jpg") + self.generateThumbnail(title, image_dir, + "thumbnail_default.jpg" if valid_cover else thumb_file) else: if False and self.verbose: self.opts.log.warn(" generating new thumbnail_default.jpg") - #title['cover'] = "%s/DefaultCover.jpg" % self.catalogPath + #title['cover'] = os.path.join(self.catalogPath,"DefaultCover.jpg") title['cover'] = cover - self.generateThumbnail(title, image_dir, "thumbnail_default.jpg") + self.generateThumbnail(title, image_dir, + "thumbnail_default.jpg" if valid_cover else thumb_file) - # Write the thumb_width to the file validating cache contents + + # Write thumb_width to the file, validating cache contents # Allows detection of aborted catalog builds with ZipFile(self.__archive_path, mode='a') as zfw: zfw.writestr('thumb_width', self.opts.thumb_width) @@ -3162,15 +3121,17 @@ then rebuild the catalog.\n''').format(author[0]) navLabelTag = Tag(ncx_soup, "navLabel") textTag = Tag(ncx_soup, "text") if book['series']: - tokens = list(book['title'].partition(':')) + series_index = str(book['series_index']) + if series_index.endswith('.0'): + series_index = series_index[:-2] if self.generateForKindle: # Don't include Author for Kindle - textTag.insert(0, NavigableString(self.formatNCXText('%s (%s)' % \ - (tokens[2].strip(), tokens[0]), dest='title'))) + textTag.insert(0, NavigableString(self.formatNCXText('%s (%s [%s])' % + (book['title'], book['series'], series_index), dest='title'))) else: # Include Author for non-Kindle - textTag.insert(0, NavigableString(self.formatNCXText('%s · %s (%s)' % \ - (tokens[2].strip(), book['author'], tokens[0]), dest='title'))) + textTag.insert(0, NavigableString(self.formatNCXText('%s (%s [%s]) · %s ' % + (book['title'], book['series'], series_index, book['author']), dest='title'))) else: if self.generateForKindle: # Don't include Author for Kindle @@ -3725,43 +3686,6 @@ then rebuild the catalog.\n''').format(author[0]) add_to_master_date_range_list(current_titles_list) current_titles_list = [book['title']] - ''' - # Add *article* entries for each populated date range - # master_date_range_list{}: [0]:titles list [1]:datestr - for books_by_date_range in master_date_range_list: - navPointByDateRangeTag = Tag(soup, 'navPoint') - navPointByDateRangeTag['class'] = "article" - navPointByDateRangeTag['id'] = "%s-ID" % books_by_date_range[1].replace(' ','') - navPointTag['playOrder'] = self.playOrder - self.playOrder += 1 - navLabelTag = Tag(soup, 'navLabel') - textTag = Tag(soup, 'text') - textTag.insert(0, NavigableString(books_by_date_range[1])) - navLabelTag.insert(0, textTag) - navPointByDateRangeTag.insert(0,navLabelTag) - contentTag = Tag(soup, 'content') - contentTag['src'] = "%s#bdr_%s" % (HTML_file, - books_by_date_range[1].replace(' ','')) - - navPointByDateRangeTag.insert(1,contentTag) - - if self.generateForKindle: - cmTag = Tag(soup, '%s' % 'calibre:meta') - cmTag['name'] = "description" - cmTag.insert(0, NavigableString(books_by_date_range[0])) - navPointByDateRangeTag.insert(2, cmTag) - - cmTag = Tag(soup, '%s' % 'calibre:meta') - cmTag['name'] = "author" - navStr = '%d titles' % books_by_date_range[2] if books_by_date_range[2] > 1 else \ - '%d title' % books_by_date_range[2] - cmTag.insert(0, NavigableString(navStr)) - navPointByDateRangeTag.insert(3, cmTag) - - navPointTag.insert(nptc, navPointByDateRangeTag) - nptc += 1 - ''' - # Create an NCX article entry for each populated day # Loop over the booksByDate list, find start of each month, # add description_preview_count titles @@ -3944,7 +3868,8 @@ then rebuild the catalog.\n''').format(author[0]) outfile = open("%s/%s.ncx" % (self.catalogPath, self.basename), 'w') outfile.write(self.ncxSoup.prettify()) - # Helpers + + # --------------- Helpers --------------- def author_to_author_sort(self, author): tokens = author.split() tokens = tokens[-1:] + tokens[:-1] @@ -3952,45 +3877,39 @@ then rebuild the catalog.\n''').format(author[0]) tokens[0] += ',' return ' '.join(tokens).capitalize() - def author_compare(self,x,y): - # Return -1 if x<y - # Return 0 if x==y - # Return 1 if x>y - - # Different authors - sort by author_sort - if x['author_sort'].capitalize() > y['author_sort'].capitalize(): - return 1 - elif x['author_sort'].capitalize() < y['author_sort'].capitalize(): - return -1 + def booksByAuthorSorter_author_sort(self, book): + ''' + Sort non-series books before series books + ''' + if not book['series']: + key = '%s %s' % (book['author_sort'], + book['title_sort'].capitalize()) else: - # Same author - if x['series'] != y['series']: - # One title is a series, the other is not - if not x['series']: - # Sort regular titles < series titles - return -1 - elif not y['series']: - return 1 + index = book['series_index'] + integer = int(index) + fraction = index-integer + series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) + key = '%s ~%s %s' % (book['author_sort'], + self.generateSortTitle(book['series']), + series_index) + return key - # Different series - if x['title_sort'].lstrip() > y['title_sort'].lstrip(): - return 1 - else: - return -1 - else: - # Same series - if x['series'] == y['series']: - if float(x['series_index']) > float(y['series_index']): - return 1 - elif float(x['series_index']) < float(y['series_index']): - return -1 - else: - return 0 - else: - if x['series'] > y['series']: - return 1 - else: - return -1 + def booksByAuthorSorter_author(self, book): + ''' + Sort non-series books before series books + ''' + if not book['series']: + key = '%s %s' % (self.author_to_author_sort(book['author']), + book['title_sort'].capitalize()) + else: + index = book['series_index'] + integer = int(index) + fraction = index-integer + series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) + key = '%s ~%s %s' % (self.author_to_author_sort(book['author']), + self.generateSortTitle(book['series']), + series_index) + return key def calculateThumbnailSize(self): ''' Calculate thumbnail dimensions based on device DPI. Scale Kindle by 50% ''' @@ -4155,6 +4074,20 @@ then rebuild the catalog.\n''').format(author[0]) # Strip white space to '' return re.sub("\W","", author) + def generateFormatArgs(self, book): + series_index = str(book['series_index']) + if series_index.endswith('.0'): + series_index = series_index[:-2] + args = dict( + title = book['title'], + series = book['series'], + series_index = series_index, + rating = self.generateRatingString(book), + rating_parens = '(%s)' % self.generateRatingString(book) if 'rating' in book else '', + pubyear = book['date'].split()[1] if book['date'] else '', + pubyear_parens = "(%s)" % book['date'].split()[1] if book['date'] else '') + return args + def generateHTMLByGenre(self, genre, section_head, books, outfile): # Write an HTML file of this genre's book list # Return a list with [(first_author, first_book), (last_author, last_book)] @@ -4201,16 +4134,6 @@ then rebuild the catalog.\n''').format(author[0]) divTag.insert(dtc,pAuthorTag) dtc += 1 - ''' - # Insert an <hr /> between non-series and series - if not current_series and non_series_books and book['series']: - # Insert an <hr /> - hrTag = Tag(soup,'hr') - hrTag['class'] = "series_divider" - divTag.insert(dtc,hrTag) - dtc += 1 - ''' - # Check for series if book['series'] and book['series'] != current_series: # Start a new series @@ -4235,17 +4158,6 @@ then rebuild the catalog.\n''').format(author[0]) pBookTag = Tag(soup, "p") ptc = 0 - ''' - # This if clause does not display MISSING_SYMBOL for wishlist items - # If this is the wishlist_tag genre, don't show missing symbols - # normalized_wishlist_tag = self.genre_tags_dict[self.opts.wishlist_tag] - if self.opts.wishlist_tag in book['tags'] and \ - self.genre_tags_dict[self.opts.wishlist_tag] != genre: - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - ''' - # book with read|reading|unread symbol or wishlist item if self.opts.wishlist_tag in book.get('tags', []): pBookTag['class'] = "wishlist_item" @@ -4271,12 +4183,18 @@ then rebuild the catalog.\n''').format(author[0]) aTag = Tag(soup, "a") if self.opts.generate_descriptions: aTag['href'] = "book_%d.html" % (int(float(book['id']))) - # Use series, series index if avail else just title + + # Generate the title from the template + args = self.generateFormatArgs(book) if current_series: - aTag.insert(0,escape(book['title'][len(book['series'])+1:])) + #aTag.insert(0,escape(book['title'][len(book['series'])+1:])) + formatted_title = self.by_genres_series_title_template.format(**args).rstrip() else: - aTag.insert(0,escape(book['title'])) + #aTag.insert(0,escape(book['title'])) + formatted_title = self.by_genres_normal_title_template.format(**args).rstrip() non_series_books += 1 + aTag.insert(0,NavigableString(escape(formatted_title))) + pBookTag.insert(ptc, aTag) ptc += 1 @@ -4322,36 +4240,21 @@ then rebuild the catalog.\n''').format(author[0]) xmlns=XHTML_NS, ) - generated_html = P('catalog/template.xhtml', + generated_html = P(os.path.join('catalog','template.xhtml'), data=True).decode('utf-8').format(**args) generated_html = substitute_entites(generated_html) return BeautifulSoup(generated_html) - if False: - print "title metadata:\n%s" % ', '.join(sorted(book.keys())) - if False: - for item in sorted(book.keys()): - try: - print "%s: %s%s" % (item, book[item][:50], '...' if len(book[item])>50 else '') - except: - print "%s: %s" % (item, book[item]) - # Generate the template arguments - css = P('catalog/stylesheet.css', data=True).decode('utf-8') - title_str = escape(book['title']) - - # Title/series + css = P(os.path.join('catalog','stylesheet.css'), data=True).decode('utf-8') + title_str = title = escape(book['title']) + series = '' + series_index = '' if book['series']: - series_id, _, title = book['title'].partition(':') - title = escape(title.strip()) series = escape(book['series']) series_index = str(book['series_index']) if series_index.endswith('.0'): series_index = series_index[:-2] - else: - title = escape(book['title']) - series = '' - series_index = '' # Author, author_prefix (read|reading|none symbol or missing symbol) author = book['author'] @@ -4392,7 +4295,10 @@ then rebuild the catalog.\n''').format(author[0]) # Date of publication pubdate = book['date'] - pubmonth, pubyear = pubdate.split(' ') + if pubdate: + pubmonth, pubyear = pubdate.split(' ') + else: + pubmonth = pubyear = '' # Thumb _soup = BeautifulSoup('<html>',selfClosingTags=['img']) @@ -4525,7 +4431,7 @@ then rebuild the catalog.\n''').format(author[0]) def generateMastheadImage(self, out_path): from calibre.ebooks.conversion.config import load_defaults from calibre.utils.fonts import fontconfig - font_path = default_font = P('fonts/liberation/LiberationSerif-Bold.ttf') + font_path = default_font = P(os.path.join('fonts','liberation','LiberationSerif-Bold.ttf')) recs = load_defaults('mobi_output') masthead_font_family = recs.get('masthead_font', 'Default') @@ -4562,16 +4468,15 @@ then rebuild the catalog.\n''').format(author[0]) draw.text((left, top), text, fill=(0,0,0), font=font) img.save(open(out_path, 'wb'), 'GIF') - def generateSeriesTitle(self, title): - if float(title['series_index']) - int(title['series_index']): - series_title = '%s %4.2f: %s' % (title['series'], - title['series_index'], - title['title']) - else: - series_title = '%s %d: %s' % (title['series'], - title['series_index'], - title['title']) - return series_title + def generateRatingString(self, book): + rating = '' + if 'rating' in book: + stars = int(book['rating']) / 2 + if stars: + star_string = self.FULL_RATING_SYMBOL * stars + empty_stars = self.EMPTY_RATING_SYMBOL * (5 - stars) + rating = '%s%s' % (star_string,empty_stars) + return rating def generateShortDescription(self, description, dest=None): # Truncate the description, on word boundaries if necessary @@ -4610,9 +4515,11 @@ then rebuild the catalog.\n''').format(author[0]) raise RuntimeError def generateSortTitle(self, title): - # Generate a string suitable for sorting from the title - # Ignore leading stop words - # Optionally convert leading numbers to strings + ''' + Generate a string suitable for sorting from the title + Ignore leading stop words + Optionally convert leading numbers to strings + ''' from calibre.ebooks.metadata import title_sort # Strip stop words @@ -4912,10 +4819,10 @@ then rebuild the catalog.\n''').format(author[0]) class NotImplementedError: def __init__(self, error): - self.error = error + self.error.append(error) def logerror(self): - self.opts.log.info('%s not implemented' % self.error) + self.opts.log.info('%s not implemented' % error) def run(self, path_to_output, opts, db, notification=DummyReporter()): opts.log = log @@ -4982,11 +4889,12 @@ then rebuild the catalog.\n''').format(author[0]) if opts_dict['ids']: build_log.append(" book count: %d" % len(opts_dict['ids'])) - sections_list = ['Authors'] ''' + sections_list = [] if opts.generate_authors: sections_list.append('Authors') ''' + sections_list = ['Authors'] if opts.generate_titles: sections_list.append('Titles') if opts.generate_genres: @@ -5042,7 +4950,7 @@ then rebuild the catalog.\n''').format(author[0]) if catalog_source_built: log.info(" Completed catalog source generation\n") else: - log.warn(" No database hits with supplied criteria") + log.warn(" *** Errors during catalog generation, check log for details ***") if catalog_source_built: recommendations = [] @@ -5072,8 +4980,6 @@ then rebuild the catalog.\n''').format(author[0]) abort_after_input_dump=False) plumber.merge_ui_recommendations(recommendations) plumber.run() - # returns to gui2.actions.catalog:catalog_generated() - return None - else: - # returns to gui2.actions.catalog:catalog_generated() - return catalog.error + + # returns to gui2.actions.catalog:catalog_generated() + return catalog.error From 30922f75f20b0c9e96adc53e0951574057ace50e Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 09:26:45 -0700 Subject: [PATCH 22/36] Fix #8430 (Economist Print Edition no longer requires a password) --- resources/recipes/economist.recipe | 9 +++++---- resources/recipes/economist_free.recipe | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/resources/recipes/economist.recipe b/resources/recipes/economist.recipe index 01ee8e0baf..95b4a2ae05 100644 --- a/resources/recipes/economist.recipe +++ b/resources/recipes/economist.recipe @@ -9,7 +9,7 @@ from calibre.web.feeds.news import BasicNewsRecipe from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.BeautifulSoup import Tag, NavigableString -import mechanize, string, urllib, time, re +import string, time, re class Economist(BasicNewsRecipe): @@ -18,19 +18,19 @@ class Economist(BasicNewsRecipe): __author__ = "Kovid Goyal" INDEX = 'http://www.economist.com/printedition' - description = ('Global news and current affairs from a European perspective.' - ' Needs a subscription from ')+INDEX + description = 'Global news and current affairs from a European perspective.' oldest_article = 7.0 cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']), dict(attrs={'class':['dblClkTrk', 'ec-article-info']})] keep_only_tags = [dict(id='ec-article-body')] - needs_subscription = True + needs_subscription = False no_stylesheets = True preprocess_regexps = [(re.compile('</html>.*', re.DOTALL), lambda x:'</html>')] + ''' def get_browser(self): br = BasicNewsRecipe.get_browser() br.open('http://www.economist.com') @@ -50,6 +50,7 @@ class Economist(BasicNewsRecipe): })) br.open(req).read() return br + ''' def parse_index(self): try: diff --git a/resources/recipes/economist_free.recipe b/resources/recipes/economist_free.recipe index 1a783521f6..321c7d29ce 100644 --- a/resources/recipes/economist_free.recipe +++ b/resources/recipes/economist_free.recipe @@ -7,12 +7,12 @@ from lxml import html class Economist(BasicNewsRecipe): - title = 'The Economist (free)' + title = 'The Economist (RSS)' language = 'en' __author__ = "Kovid Goyal" description = ('Global news and current affairs from a European perspective.' - ' Much slower than the subscription based version.') + ' Much slower than the print edition based version.') oldest_article = 7.0 cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' From 0d91ea7055c24b98b9117517abf669cae3f6f53a Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 09:39:06 -0700 Subject: [PATCH 23/36] When auto converting books and the device is unplugged, do not raise an error. Fixes #8426 (Exception when attempting to send books to a Kindle that was sleeping) --- src/calibre/gui2/device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 944ce03305..734d8cd56c 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1018,7 +1018,8 @@ class DeviceMixin(object): # {{{ ids = [self.library_view.model().id(r) \ for r in self.library_view.selectionModel().selectedRows()] \ if send_ids is None else send_ids - if not self.device_manager or not ids or len(ids) == 0: + if not self.device_manager or not ids or len(ids) == 0 or \ + not self.device_manager.is_device_connected: return settings = self.device_manager.device.settings() From 61d365c25b35a265175f1c9d26ae573fb4998965 Mon Sep 17 00:00:00 2001 From: GRiker <griker@hotmail.com> Date: Mon, 17 Jan 2011 09:59:46 -0700 Subject: [PATCH 24/36] GwR catalog 1.0 revisions --- src/calibre/library/catalog.py | 46 ++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index f1c5e3ae65..cd50cf4378 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -1429,12 +1429,29 @@ class EPUB_MOBI(CatalogPlugin): self.updateProgressFullStep("Sorting database") self.booksByAuthor = list(self.booksByTitle) - self.booksByAuthor = sorted(self.booksByAuthor, key=self.booksByAuthorSorter_author) -# for book in self.booksByAuthor: -# print "{0:<30} {1:<30} {2:<30}".format(book['title'],book['author'],book['author_sort']) -# print -# stop + # Test for author_sort mismatches + self.booksByAuthor = sorted(self.booksByAuthor, key=self.booksByAuthorSorter_author) + # Build the unique_authors set from existing data + authors = [(record['author'], record['author_sort']) for record in self.booksByAuthor] + current_author = authors[0] + for (i,author) in enumerate(authors): + if author != current_author and i: + # Exit if author matches previous, but author_sort doesn't match + if author[0] == current_author[0]: + error_msg = _(''' +Inconsistent Author Sort values for Author '{0}' ('{1}' <> '{2}'), unable to build catalog.\n +Select all books by '{0}', apply correct Author Sort value in Edit Metadata dialog, +then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) + self.opts.log.warn('\n*** Metadata error ***') + self.opts.log.warn(error_msg) + + self.error.append('Metadata error') + self.error.append(error_msg) + return False + + + self.booksByAuthor = sorted(self.booksByAuthor, key=self.booksByAuthorSorter_author_sort) # Build the unique_authors set from existing data authors = [(record['author'], record['author_sort'].capitalize()) for record in self.booksByAuthor] @@ -1450,20 +1467,6 @@ class EPUB_MOBI(CatalogPlugin): # Note that current_author and author are tuples: (friendly, sort) multiple_authors = True - if author != current_author and i: - # Exit if author matches previous, but author_sort doesn't match - if author[0] == current_author[0]: - error_msg = _(''' -Inconsistent Author Sort values for Author '{0}', unable to continue building catalog.\n -Select all books by '{0}', apply correct Author Sort value in Edit Metadata dialog, -then rebuild the catalog.\n''').format(author[0]) - self.opts.log.warn('\n*** Metadata error ***') - self.opts.log.warn(error_msg) - - self.error.append('Metadata error') - self.error.append(error_msg) - return False - # New author, save the previous author/sort/count unique_authors.append((current_author[0], icu_title(current_author[1]), books_by_current_author)) @@ -1939,7 +1942,8 @@ then rebuild the catalog.\n''').format(author[0]) current_author = '' current_letter = '' current_series = None - for book in sorted(self.booksByAuthor, key = self.booksByAuthorSorter_author_sort): + #for book in sorted(self.booksByAuthor, key = self.booksByAuthorSorter_author_sort): + for book in self.booksByAuthor: book_count += 1 if self.letter_or_symbol(book['author_sort'][0].upper()) != current_letter : @@ -2118,7 +2122,7 @@ then rebuild the catalog.\n''').format(author[0]) def add_books_to_HTML_by_month(this_months_list, dtc): if len(this_months_list): - this_months_list = sorted(this_months_list, key=self.booksByAuthorSorter_author_sort) + #this_months_list = sorted(this_months_list, key=self.booksByAuthorSorter_author_sort) # Create a new month anchor date_string = strftime(u'%B %Y', current_date.timetuple()) From 1d24b14bc21a933067cc140760b59af0c4d709b2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 09:59:50 -0700 Subject: [PATCH 25/36] ... --- src/calibre/manual/faq.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index ee72bf6fdb..b473893673 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -101,6 +101,17 @@ We just need some information from you: Once you send us the output for a particular operating system, support for the device in that operating system will appear in the next release of |app|. +My device is not being detected by |app|? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Follow these steps to find the problem: + + * Make sure that you are connecting only a single device to your computer at a time. Do not have another |app| supported device like an iPhone/iPad etc. at the same time. + * Make sure you are running the latest version of |app|. The latest version can always be downloaded from `http://calibre-ebook.com/download`_. + * Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is + * In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled. + * If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `http://bugs.calibre-ebook.com`_. + How does |app| manage collections on my SONY reader? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 5e2a2d71a61bfdb85d191461a2d7be5a252436e6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 10:11:22 -0700 Subject: [PATCH 26/36] Better error message in debug log when failed to fetch a news article --- src/calibre/web/feeds/news.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index dd32d3749f..6215132e4b 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -839,7 +839,13 @@ class BasicNewsRecipe(Recipe): fetcher.image_url_processor = self.image_url_processor res, path, failures = fetcher.start_fetch(url), fetcher.downloaded_paths, fetcher.failed_links if not res or not os.path.exists(res): - raise Exception(_('Could not fetch article. Run with -vv to see the reason')) + msg = _('Could not fetch article.') + ' ' + if self.debug: + msg += _('The debug traceback is available earlier in this log') + else: + msg += _('Run with -vv to see the reason') + raise Exception(msg) + return res, path, failures def fetch_article(self, url, dir, f, a, num_of_feeds): @@ -901,7 +907,7 @@ class BasicNewsRecipe(Recipe): if self.test: feeds = feeds[:2] self.has_single_feed = len(feeds) == 1 - + index = os.path.join(self.output_dir, 'index.html') html = self.feeds2index(feeds) From 46ab37e98f310c8134c44c79bf2da14422ec0bd7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 10:17:34 -0700 Subject: [PATCH 27/36] iHNed by Karel Bilek --- resources/recipes/ihned.recipe | 182 +++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 resources/recipes/ihned.recipe diff --git a/resources/recipes/ihned.recipe b/resources/recipes/ihned.recipe new file mode 100644 index 0000000000..daf63e19ed --- /dev/null +++ b/resources/recipes/ihned.recipe @@ -0,0 +1,182 @@ +import re, time +from calibre import strftime +from calibre.web.feeds.recipes import BasicNewsRecipe + +class IHNed(BasicNewsRecipe): + + + stahnout_vsechny = False + #True = stahuje vsechny z homepage + #False = stahuje pouze dnesni clanky (ze dne, kdy je skript spusten) + + title = 'iHNed' + __author__ = 'Karel Bílek' + language = 'cs' + description = 'Zprávy z iHNed.cz' + timefmt = ' [%a, %d %b, %Y]' + needs_subscription = False + remove_tags = [dict(attrs={'class':['borderbottom', 'web', 'foot', 'reklama', 'd-elm d-rellinks', 'd-elm']}), + dict(style=['text-align: center;']), + dict(id=['r-bfull']), + dict(name=['script', 'noscript', 'style'])] + encoding = 'windows-1250' + no_stylesheets = True + remove_tags_before = dict(attrs={'class':'d-nadtit'}) + remove_tags_after = dict(attrs={'class':'like'}) + + conversion_options = { + 'linearize_tables' : True, + } + + + + def preprocess_html(self, soup): + + def makeurl(wat): + return "http://ihned.cz"+wat; + + for h1 in soup.findAll('h1'): + a = h1.find('a') + if a: + string = a.string + if string: + soup.a.replaceWith(string) + for a in soup.findAll('a', href=True) : + cil = str(a['href']) + if cil.startswith("/") or cil.startswith("index"): + a['href'] = makeurl(cil) + return soup + + + def parse_index(self): + + def makeurl(wat): + if wat.startswith("/") or wat.startswith("index"): + return "http://ihned.cz"+wat; + else: + return wat + + + articles = {} #vysledek, asi + key = None #soucasna sekce + ans = [] #vsechny sekce + + articles["Hlavní"] = [] + ans.append("Hlavní") + + was = {} + + def parse_subpage(url, name): + articles[name] = [] + ans.append(name) + + + soup = self.index_to_soup(url) + otvirak = soup.find(True, attrs={'class':['otv']}) + if otvirak: + + #the code is copypasted here because I don't know python. simple as that. + a = otvirak.find('a', href=True) + title = self.tag_to_string(a, use_alt=True).strip() + txt = otvirak.find(True, attrs={'class':['txt']}) + description = '' + if txt: + match = re.match(r'<div class="txt">\s*([^<]*)\s*<a', str(txt), re.L) + if match: + description = match.group(1) + + pubdate = strftime('%d. %m.') + if not title in was: + articles[name].append( + dict(title=title, url=makeurl(a['href']), date=pubdate, + description=description, + content='')) + + otv234 = soup.find(True, attrs={'class':['otv234', 'col2a']}) + if otv234: + for ow in otv234.findAll(True, attrs={'class':['ow']}): + a = ow.find('a', href=True) + title = self.tag_to_string(a, use_alt=True).strip() + description='' + prx = ow.find(True, attrs={'class':['prx']}); + if prx: + description = str(prx.string) + nfo = ow.find(True, attrs={'class':['nfo']}); + pubdate = '' + if nfo: + dtime = time.localtime(); + day = dtime[2] + month = dtime[1] + + pubdate = strftime('%d. %m.') + + match = re.search(r'([0-9]*)\.([0-9]*)\.', str(nfo)) + + if self.stahnout_vsechny or (int(day) == int(match.group(1)) and int(month) == int(match.group(2))): + if not title in was: + articles[name].append( + dict(title=title, url=makeurl(a['href']), date=pubdate, + description=description, + content='')) + + + + + + + soup = self.index_to_soup('http://ihned.cz/') + otvirak = soup.find(True, attrs={'class':['otv']}) + if otvirak: + a = otvirak.find('a', href=True) + title = self.tag_to_string(a, use_alt=True).strip() + txt = otvirak.find(True, attrs={'class':['txt']}) + description = '' + if txt: + match = re.match(r'<div class="txt">\s*([^<]*)\s*<a', str(txt), re.L) + if match: + description = match.group(1) + + pubdate = strftime('%d. %m.') + feed = "Hlavní" + articles[feed].append( + dict(title=title, url=(a['href']), date=pubdate, + description=description, + content='')) + was[title]=1 + + otvirak2345 = soup.find(True, attrs={'class':['otv2345']}) + if otvirak2345: + for otv2 in otvirak2345.findAll(True, attrs={'class':['otv2-5']}): + a = otv2.find('a', attrs={'class':['tit2']}, href=True) + title = self.tag_to_string(a, use_alt=True).strip() + description='' + span = otv2.find('span'); + if span: + match = re.match(r'<span>\s*([^<]*)\s*<a', str(span), re.L) + if match: + description = match.group(1) + feed = "Hlavní" + pubdate = strftime('%d. %m.') + articles[feed].append( + dict(title=title, url=(a['href']), date=pubdate, + description=description, + content='')) + was[title]=1 + + + parse_subpage("http://komentare.ihned.cz/", "Komentáře") + parse_subpage("http://domaci.ihned.cz", "Domácí") + parse_subpage("http://ekonomika.ihned.cz", "Ekonomika") + parse_subpage("http://zahranicni.ihned.cz/", "Zahraničí"); + parse_subpage("http://finweb.ihned.cz/", "Finance"); + parse_subpage("http://digiweb.ihned.cz/", "DigiWeb"); + parse_subpage("http://kultura.ihned.cz/", "Kultura") + parse_subpage("http://sport.ihned.cz/", "Sport"); + + #seradi kategorie + ans = self.sort_index_by(ans, {'Hlavni':1, 'Domácí':2, 'Ekonomika':5, 'Zahraničí':3, 'Finance':6, 'DigiWeb':7, 'Kultura':8, 'Sport':9, 'Komentáře':4}) + + #vrati, ale pouze, kdyz je v kategoriich... + ans = [(key, articles[key]) for key in ans if articles.has_key(key)] + return ans + From e18e5a5db9fc1682fafed636d2d6187cebbf8a0e Mon Sep 17 00:00:00 2001 From: GRiker <griker@hotmail.com> Date: Mon, 17 Jan 2011 10:29:38 -0700 Subject: [PATCH 28/36] GwR catalog 1.0 revisions --- src/calibre/gui2/actions/catalog.py | 2 ++ src/calibre/library/catalog.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py index 1650c80d70..be6e7bfe60 100644 --- a/src/calibre/gui2/actions/catalog.py +++ b/src/calibre/gui2/actions/catalog.py @@ -56,6 +56,8 @@ class GenerateCatalogAction(InterfaceAction): def catalog_generated(self, job): if job.result: # Problems during catalog generation + # jobs.results is a list - the first entry is the intended title for the dialog + # Subsequent strings are error messages dialog_title = job.result.pop(0) if re.match('warning:', job.result[0].lower()): job.result.append("Catalog generation complete.") diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index cd50cf4378..cf02e9b792 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -1637,7 +1637,10 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) title['title_sort'][0:40])).decode('mac-roman')) return True else: - self.error.append( _("No books found to catalog.\nCheck 'Excluded books' criteria in E-book options.")) + error_msg = _("No books found to catalog.\nCheck 'Excluded books' criteria in E-book options.\n") + self.opts.log.error('*** ' + error_msg + ' ***') + self.error.append(_('No books available to include in catalog')) + self.error.append(error_msg) return False def fetchBookmarks(self): @@ -3164,8 +3167,13 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Add the author tag cmTag = Tag(ncx_soup, '%s' % 'calibre:meta') cmTag['name'] = "author" - navStr = '%s | %s' % (self.formatNCXText(book['author'], dest='author'), - book['date'].split()[1]) + + if book['date']: + navStr = '%s | %s' % (self.formatNCXText(book['author'], dest='author'), + book['date'].split()[1]) + else: + navStr = '%s' % (self.formatNCXText(book['author'], dest='author')) + if 'tags' in book and len(book['tags']): navStr = self.formatNCXText(navStr + ' | ' + ' · '.join(sorted(book['tags'])), dest='author') cmTag.insert(0, NavigableString(navStr)) From 5f2e4a1f3f0a7c596cf0db87dfcebdc7b274e7f6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 10:34:38 -0700 Subject: [PATCH 29/36] kath.net bu Bobus --- resources/recipes/kath_net.recipe | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 resources/recipes/kath_net.recipe diff --git a/resources/recipes/kath_net.recipe b/resources/recipes/kath_net.recipe new file mode 100644 index 0000000000..7c469adbe8 --- /dev/null +++ b/resources/recipes/kath_net.recipe @@ -0,0 +1,16 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1295262156(BasicNewsRecipe): + title = u'kath.net' + __author__ = 'Bobus' + oldest_article = 7 + max_articles_per_feed = 100 + + feeds = [(u'kath.net', u'http://www.kath.net/2005/xml/index.xml')] + + + def print_version(self, url): + return url+"&print=yes" + + extra_css = 'td.textb {font-size: medium;}' + From f965037fb44184957d9bc20dc3efb1ce1adee6a9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 12:22:30 -0700 Subject: [PATCH 30/36] MOBI Output: Fix bug that could cause a link pointing to the start of a section to go to a point later in the section is the section contained an empty id attribute --- src/calibre/ebooks/mobi/writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index cd6674c2e2..ed102ecc80 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -251,7 +251,7 @@ class Serializer(object): tag = prefixname(elem.tag, nsrmap) # Previous layers take care of @name id = elem.attrib.pop('id', None) - if id is not None: + if id: href = '#'.join((item.href, id)) offset = self.anchor_offset or buffer.tell() self.id_offsets[urlnormalize(href)] = offset From aa28b379517028b5e7951c8aa94f2226a99c918d Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 13:03:34 -0700 Subject: [PATCH 31/36] Fix #8424 (Dilbert retrieval fails) --- resources/recipes/dilbert.recipe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/recipes/dilbert.recipe b/resources/recipes/dilbert.recipe index 2c3268da2f..56aa4af8c9 100644 --- a/resources/recipes/dilbert.recipe +++ b/resources/recipes/dilbert.recipe @@ -28,7 +28,7 @@ class DilbertBig(BasicNewsRecipe): ,'publisher' : publisher } - feeds = [(u'Dilbert', u'http://feeds.dilbert.com/DilbertDailyStrip' )] + feeds = [(u'Dilbert', u'http://feed.dilbert.com/dilbert/daily_strip' )] def get_article_url(self, article): return article.get('feedburner_origlink', None) From 84d1dd94d23db7b53f47051ecb3cfd5c47965f0b Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 13:10:10 -0700 Subject: [PATCH 32/36] Make postprocess_html in the NY Times recipes more robust --- resources/recipes/nytimes.recipe | 204 +++++++++++++------------ resources/recipes/nytimes_sub.recipe | 214 +++++++++++++++------------ 2 files changed, 229 insertions(+), 189 deletions(-) diff --git a/resources/recipes/nytimes.recipe b/resources/recipes/nytimes.recipe index 6f80f4f85f..7e313e5727 100644 --- a/resources/recipes/nytimes.recipe +++ b/resources/recipes/nytimes.recipe @@ -586,105 +586,125 @@ class NYTimes(BasicNewsRecipe): return self.strip_anchors(soup) def postprocess_html(self,soup, True): + try: + if self.one_picture_per_article: + # Remove all images after first + largeImg = soup.find(True, {'class':'articleSpanImage'}) + inlineImgs = soup.findAll(True, {'class':'inlineImage module'}) + if largeImg: + for inlineImg in inlineImgs: + inlineImg.extract() + else: + if inlineImgs: + firstImg = inlineImgs[0] + for inlineImg in inlineImgs[1:]: + inlineImg.extract() + # Move firstImg before article body + cgFirst = soup.find(True, {'class':re.compile('columnGroup *first')}) + if cgFirst: + # Strip all sibling NavigableStrings: noise + navstrings = cgFirst.findAll(text=True, recursive=False) + [ns.extract() for ns in navstrings] + headline_found = False + tag = cgFirst.find(True) + insertLoc = 0 + while True: + insertLoc += 1 + if hasattr(tag,'class') and tag['class'] == 'articleHeadline': + headline_found = True + break + tag = tag.nextSibling + if not tag: + headline_found = False + break + if headline_found: + cgFirst.insert(insertLoc,firstImg) + else: + self.log(">>> No class:'columnGroup first' found <<<") + except: + self.log("ERROR: One picture per article in postprocess_html") - if self.one_picture_per_article: - # Remove all images after first - largeImg = soup.find(True, {'class':'articleSpanImage'}) - inlineImgs = soup.findAll(True, {'class':'inlineImage module'}) - if largeImg: - for inlineImg in inlineImgs: - inlineImg.extract() - else: - if inlineImgs: - firstImg = inlineImgs[0] - for inlineImg in inlineImgs[1:]: - inlineImg.extract() - # Move firstImg before article body - cgFirst = soup.find(True, {'class':re.compile('columnGroup *first')}) - if cgFirst: - # Strip all sibling NavigableStrings: noise - navstrings = cgFirst.findAll(text=True, recursive=False) - [ns.extract() for ns in navstrings] - headline_found = False - tag = cgFirst.find(True) - insertLoc = 0 - while True: - insertLoc += 1 - if hasattr(tag,'class') and tag['class'] == 'articleHeadline': - headline_found = True - break - tag = tag.nextSibling - if not tag: - headline_found = False - break - if headline_found: - cgFirst.insert(insertLoc,firstImg) - else: - self.log(">>> No class:'columnGroup first' found <<<") + try: + # Change captions to italic + for caption in soup.findAll(True, {'class':'caption'}) : + if caption and len(caption) > 0: + cTag = Tag(soup, "p", [("class", "caption")]) + c = self.fixChars(self.tag_to_string(caption,use_alt=False)).strip() + mp_off = c.find("More Photos") + if mp_off >= 0: + c = c[:mp_off] + cTag.insert(0, c) + caption.replaceWith(cTag) + except: + self.log("ERROR: Problem in change captions to italic") - # Change captions to italic - for caption in soup.findAll(True, {'class':'caption'}) : - if caption and caption.contents[0]: - cTag = Tag(soup, "p", [("class", "caption")]) - c = self.fixChars(self.tag_to_string(caption,use_alt=False)).strip() - mp_off = c.find("More Photos") - if mp_off >= 0: - c = c[:mp_off] - cTag.insert(0, c) - caption.replaceWith(cTag) + try: + # Change <nyt_headline> to <h2> + h1 = soup.find('h1') + if h1: + headline = h1.find("nyt_headline") + if headline: + tag = Tag(soup, "h2") + tag['class'] = "headline" + tag.insert(0, self.fixChars(headline.contents[0])) + h1.replaceWith(tag) + else: + # Blog entry - replace headline, remove <hr> tags + headline = soup.find('title') + if headline: + tag = Tag(soup, "h2") + tag['class'] = "headline" + tag.insert(0, self.fixChars(headline.contents[0])) + soup.insert(0, tag) + hrs = soup.findAll('hr') + for hr in hrs: + hr.extract() + except: + self.log("ERROR: Problem in Change <nyt_headline> to <h2>") - # Change <nyt_headline> to <h2> - h1 = soup.find('h1') - if h1: - headline = h1.find("nyt_headline") - if headline: - tag = Tag(soup, "h2") - tag['class'] = "headline" - tag.insert(0, self.fixChars(headline.contents[0])) - h1.replaceWith(tag) - else: - # Blog entry - replace headline, remove <hr> tags - headline = soup.find('title') - if headline: - tag = Tag(soup, "h2") - tag['class'] = "headline" - tag.insert(0, self.fixChars(headline.contents[0])) - soup.insert(0, tag) - hrs = soup.findAll('hr') - for hr in hrs: - hr.extract() + try: + # Change <h1> to <h3> - used in editorial blogs + masthead = soup.find("h1") + if masthead: + # Nuke the href + if masthead.a: + del(masthead.a['href']) + tag = Tag(soup, "h3") + tag.insert(0, self.fixChars(masthead.contents[0])) + masthead.replaceWith(tag) + except: + self.log("ERROR: Problem in Change <h1> to <h3> - used in editorial blogs") - # Change <h1> to <h3> - used in editorial blogs - masthead = soup.find("h1") - if masthead: - # Nuke the href - if masthead.a: - del(masthead.a['href']) - tag = Tag(soup, "h3") - tag.insert(0, self.fixChars(masthead.contents[0])) - masthead.replaceWith(tag) + try: + # Change <span class="bold"> to <b> + for subhead in soup.findAll(True, {'class':'bold'}) : + if subhead.contents: + bTag = Tag(soup, "b") + bTag.insert(0, subhead.contents[0]) + subhead.replaceWith(bTag) + except: + self.log("ERROR: Problem in Change <h1> to <h3> - used in editorial blogs") - # Change <span class="bold"> to <b> - for subhead in soup.findAll(True, {'class':'bold'}) : - if subhead.contents: - bTag = Tag(soup, "b") - bTag.insert(0, subhead.contents[0]) - subhead.replaceWith(bTag) + try: + divTag = soup.find('div',attrs={'id':'articleBody'}) + if divTag: + divTag['class'] = divTag['id'] + except: + self.log("ERROR: Problem in soup.find(div,attrs={id:articleBody})") - divTag = soup.find('div',attrs={'id':'articleBody'}) - if divTag: - divTag['class'] = divTag['id'] + try: + # Add class="authorId" to <div> so we can format with CSS + divTag = soup.find('div',attrs={'id':'authorId'}) + if divTag and divTag.contents[0]: + tag = Tag(soup, "p") + tag['class'] = "authorId" + tag.insert(0, self.fixChars(self.tag_to_string(divTag.contents[0], + use_alt=False))) + divTag.replaceWith(tag) + except: + self.log("ERROR: Problem in Add class=authorId to <div> so we can format with CSS") - # Add class="authorId" to <div> so we can format with CSS - divTag = soup.find('div',attrs={'id':'authorId'}) - if divTag and divTag.contents[0]: - tag = Tag(soup, "p") - tag['class'] = "authorId" - tag.insert(0, self.fixChars(self.tag_to_string(divTag.contents[0], - use_alt=False))) - divTag.replaceWith(tag) - - return soup + return soup def populate_article_metadata(self, article, soup, first): shortparagraph = "" diff --git a/resources/recipes/nytimes_sub.recipe b/resources/recipes/nytimes_sub.recipe index 8ac7c735f7..8f92852237 100644 --- a/resources/recipes/nytimes_sub.recipe +++ b/resources/recipes/nytimes_sub.recipe @@ -586,105 +586,125 @@ class NYTimes(BasicNewsRecipe): return self.strip_anchors(soup) def postprocess_html(self,soup, True): + try: + if self.one_picture_per_article: + # Remove all images after first + largeImg = soup.find(True, {'class':'articleSpanImage'}) + inlineImgs = soup.findAll(True, {'class':'inlineImage module'}) + if largeImg: + for inlineImg in inlineImgs: + inlineImg.extract() + else: + if inlineImgs: + firstImg = inlineImgs[0] + for inlineImg in inlineImgs[1:]: + inlineImg.extract() + # Move firstImg before article body + cgFirst = soup.find(True, {'class':re.compile('columnGroup *first')}) + if cgFirst: + # Strip all sibling NavigableStrings: noise + navstrings = cgFirst.findAll(text=True, recursive=False) + [ns.extract() for ns in navstrings] + headline_found = False + tag = cgFirst.find(True) + insertLoc = 0 + while True: + insertLoc += 1 + if hasattr(tag,'class') and tag['class'] == 'articleHeadline': + headline_found = True + break + tag = tag.nextSibling + if not tag: + headline_found = False + break + if headline_found: + cgFirst.insert(insertLoc,firstImg) + else: + self.log(">>> No class:'columnGroup first' found <<<") + except: + self.log("ERROR: One picture per article in postprocess_html") + + try: + # Change captions to italic + for caption in soup.findAll(True, {'class':'caption'}) : + if caption and len(caption) > 0: + cTag = Tag(soup, "p", [("class", "caption")]) + c = self.fixChars(self.tag_to_string(caption,use_alt=False)).strip() + mp_off = c.find("More Photos") + if mp_off >= 0: + c = c[:mp_off] + cTag.insert(0, c) + caption.replaceWith(cTag) + except: + self.log("ERROR: Problem in change captions to italic") + + try: + # Change <nyt_headline> to <h2> + h1 = soup.find('h1') + if h1: + headline = h1.find("nyt_headline") + if headline: + tag = Tag(soup, "h2") + tag['class'] = "headline" + tag.insert(0, self.fixChars(headline.contents[0])) + h1.replaceWith(tag) + else: + # Blog entry - replace headline, remove <hr> tags + headline = soup.find('title') + if headline: + tag = Tag(soup, "h2") + tag['class'] = "headline" + tag.insert(0, self.fixChars(headline.contents[0])) + soup.insert(0, tag) + hrs = soup.findAll('hr') + for hr in hrs: + hr.extract() + except: + self.log("ERROR: Problem in Change <nyt_headline> to <h2>") - if self.one_picture_per_article: - # Remove all images after first - largeImg = soup.find(True, {'class':'articleSpanImage'}) - inlineImgs = soup.findAll(True, {'class':'inlineImage module'}) - if largeImg: - for inlineImg in inlineImgs: - inlineImg.extract() - else: - if inlineImgs: - firstImg = inlineImgs[0] - for inlineImg in inlineImgs[1:]: - inlineImg.extract() - # Move firstImg before article body - cgFirst = soup.find(True, {'class':re.compile('columnGroup *first')}) - if cgFirst: - # Strip all sibling NavigableStrings: noise - navstrings = cgFirst.findAll(text=True, recursive=False) - [ns.extract() for ns in navstrings] - headline_found = False - tag = cgFirst.find(True) - insertLoc = 0 - while True: - insertLoc += 1 - if hasattr(tag,'class') and tag['class'] == 'articleHeadline': - headline_found = True - break - tag = tag.nextSibling - if not tag: - headline_found = False - break - if headline_found: - cgFirst.insert(insertLoc,firstImg) - else: - self.log(">>> No class:'columnGroup first' found <<<") + try: + # Change <h1> to <h3> - used in editorial blogs + masthead = soup.find("h1") + if masthead: + # Nuke the href + if masthead.a: + del(masthead.a['href']) + tag = Tag(soup, "h3") + tag.insert(0, self.fixChars(masthead.contents[0])) + masthead.replaceWith(tag) + except: + self.log("ERROR: Problem in Change <h1> to <h3> - used in editorial blogs") - # Change captions to italic - for caption in soup.findAll(True, {'class':'caption'}) : - if caption and caption.contents[0]: - cTag = Tag(soup, "p", [("class", "caption")]) - c = self.fixChars(self.tag_to_string(caption,use_alt=False)).strip() - mp_off = c.find("More Photos") - if mp_off >= 0: - c = c[:mp_off] - cTag.insert(0, c) - caption.replaceWith(cTag) - - # Change <nyt_headline> to <h2> - h1 = soup.find('h1') - if h1: - headline = h1.find("nyt_headline") - if headline: - tag = Tag(soup, "h2") - tag['class'] = "headline" - tag.insert(0, self.fixChars(headline.contents[0])) - h1.replaceWith(tag) - else: - # Blog entry - replace headline, remove <hr> tags - headline = soup.find('title') - if headline: - tag = Tag(soup, "h2") - tag['class'] = "headline" - tag.insert(0, self.fixChars(headline.contents[0])) - soup.insert(0, tag) - hrs = soup.findAll('hr') - for hr in hrs: - hr.extract() - - # Change <h1> to <h3> - used in editorial blogs - masthead = soup.find("h1") - if masthead: - # Nuke the href - if masthead.a: - del(masthead.a['href']) - tag = Tag(soup, "h3") - tag.insert(0, self.fixChars(masthead.contents[0])) - masthead.replaceWith(tag) - - # Change <span class="bold"> to <b> - for subhead in soup.findAll(True, {'class':'bold'}) : - if subhead.contents: - bTag = Tag(soup, "b") - bTag.insert(0, subhead.contents[0]) - subhead.replaceWith(bTag) - - divTag = soup.find('div',attrs={'id':'articleBody'}) - if divTag: - divTag['class'] = divTag['id'] - - # Add class="authorId" to <div> so we can format with CSS - divTag = soup.find('div',attrs={'id':'authorId'}) - if divTag and divTag.contents[0]: - tag = Tag(soup, "p") - tag['class'] = "authorId" - tag.insert(0, self.fixChars(self.tag_to_string(divTag.contents[0], - use_alt=False))) - divTag.replaceWith(tag) - - return soup + try: + # Change <span class="bold"> to <b> + for subhead in soup.findAll(True, {'class':'bold'}) : + if subhead.contents: + bTag = Tag(soup, "b") + bTag.insert(0, subhead.contents[0]) + subhead.replaceWith(bTag) + except: + self.log("ERROR: Problem in Change <h1> to <h3> - used in editorial blogs") + + try: + divTag = soup.find('div',attrs={'id':'articleBody'}) + if divTag: + divTag['class'] = divTag['id'] + except: + self.log("ERROR: Problem in soup.find(div,attrs={id:articleBody})") + + try: + # Add class="authorId" to <div> so we can format with CSS + divTag = soup.find('div',attrs={'id':'authorId'}) + if divTag and divTag.contents[0]: + tag = Tag(soup, "p") + tag['class'] = "authorId" + tag.insert(0, self.fixChars(self.tag_to_string(divTag.contents[0], + use_alt=False))) + divTag.replaceWith(tag) + except: + self.log("ERROR: Problem in Add class=authorId to <div> so we can format with CSS") + + return soup def populate_article_metadata(self, article, soup, first): shortparagraph = "" try: From 927c389e91ffe47880ed1b0949b421d449f7ad4b Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 13:20:12 -0700 Subject: [PATCH 33/36] Fix #8436 (Add tag when using Add ISBN Dialog) --- src/calibre/gui2/actions/add.py | 9 ++-- src/calibre/gui2/dialogs/add_from_isbn.py | 7 +++ src/calibre/gui2/dialogs/add_from_isbn.ui | 52 +++++++++++++++++++---- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 9917c542ae..6fa53d6290 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -91,13 +91,14 @@ class AddAction(InterfaceAction): self.gui.library_view.model().db.import_book(MetaInformation(None), []) self.gui.library_view.model().books_added(num) - def add_isbns(self, books): + def add_isbns(self, books, add_tags=[]): from calibre.ebooks.metadata import MetaInformation ids = set([]) + db = self.gui.library_view.model().db + for x in books: mi = MetaInformation(None) mi.isbn = x['isbn'] - db = self.gui.library_view.model().db if x['path'] is not None: ids.add(db.import_book(mi, [x['path']])) else: @@ -109,6 +110,8 @@ class AddAction(InterfaceAction): self.gui.iactions['Edit Metadata'].do_download_metadata(ids) finally: config['overwrite_author_title_metadata'] = orig + if add_tags and ids: + db.bulk_modify_tags(ids, add=add_tags) def files_dropped(self, paths): @@ -166,7 +169,7 @@ class AddAction(InterfaceAction): from calibre.gui2.dialogs.add_from_isbn import AddFromISBN d = AddFromISBN(self.gui) if d.exec_() == d.Accepted: - self.add_isbns(d.books) + self.add_isbns(d.books, add_tags=d.set_tags) def add_books(self, *args): ''' diff --git a/src/calibre/gui2/dialogs/add_from_isbn.py b/src/calibre/gui2/dialogs/add_from_isbn.py index f93cddecd5..433b70291c 100644 --- a/src/calibre/gui2/dialogs/add_from_isbn.py +++ b/src/calibre/gui2/dialogs/add_from_isbn.py @@ -12,6 +12,7 @@ from PyQt4.Qt import QDialog, QApplication from calibre.gui2.dialogs.add_from_isbn_ui import Ui_Dialog from calibre.ebooks.metadata import check_isbn from calibre.constants import iswindows +from calibre.gui2 import gprefs class AddFromISBN(QDialog, Ui_Dialog): @@ -25,7 +26,9 @@ class AddFromISBN(QDialog, Ui_Dialog): self.isbns = [] self.books = [] + self.set_tags = [] self.paste_button.clicked.connect(self.paste) + self.add_tags.setText(', '.join(gprefs.get('add from ISBN tags', []))) def paste(self, *args): app = QApplication.instance() @@ -37,6 +40,10 @@ class AddFromISBN(QDialog, Ui_Dialog): self.isbn_box.setPlainText(new) def accept(self, *args): + tags = unicode(self.add_tags.text()).strip().split(',') + tags = list(filter(None, [x.strip() for x in tags])) + gprefs['add from ISBN tags'] = tags + self.set_tags = tags for line in unicode(self.isbn_box.toPlainText()).strip().splitlines(): line = line.strip() if not line: diff --git a/src/calibre/gui2/dialogs/add_from_isbn.ui b/src/calibre/gui2/dialogs/add_from_isbn.ui index e37c4ed769..f598e6f1d8 100644 --- a/src/calibre/gui2/dialogs/add_from_isbn.ui +++ b/src/calibre/gui2/dialogs/add_from_isbn.ui @@ -18,8 +18,19 @@ <normaloff>:/images/add_book.png</normaloff>:/images/add_book.png</iconset> </property> <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QPlainTextEdit" name="isbn_box"/> + <item row="0" column="0" rowspan="2"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QPlainTextEdit" name="isbn_box"/> + </item> + <item> + <widget class="QPushButton" name="paste_button"> + <property name="text"> + <string>&Paste from clipboard</string> + </property> + </widget> + </item> + </layout> </item> <item row="0" column="1"> <widget class="QLabel" name="label"> @@ -34,6 +45,36 @@ </property> </widget> </item> + <item row="1" column="1"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>&Tags to set on created book entries:</string> + </property> + <property name="buddy"> + <cstring>add_tags</cstring> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="add_tags"/> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> <item row="2" column="0" colspan="2"> <widget class="QDialogButtonBox" name="buttonBox"> <property name="orientation"> @@ -44,13 +85,6 @@ </property> </widget> </item> - <item row="1" column="0"> - <widget class="QPushButton" name="paste_button"> - <property name="text"> - <string>&Paste from clipboard</string> - </property> - </widget> - </item> </layout> </widget> <resources> From e1fd50d72f2eefb70928d20dab094f0402172bae Mon Sep 17 00:00:00 2001 From: GRiker <griker@hotmail.com> Date: Mon, 17 Jan 2011 15:39:23 -0700 Subject: [PATCH 34/36] GwR fix for TOC discontinuity, default cover swapping --- src/calibre/library/catalog.py | 37 ++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index fa5041bfec..13df6625d4 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -1360,6 +1360,7 @@ class EPUB_MOBI(CatalogPlugin): return False self.fetchBookmarks() if self.opts.generate_descriptions: + self.generateThumbnails() self.generateHTMLDescriptions() self.generateHTMLByAuthor() if self.opts.generate_titles: @@ -1372,8 +1373,7 @@ class EPUB_MOBI(CatalogPlugin): self.generateHTMLByDateAdded() if self.generateRecentlyRead: self.generateHTMLByDateRead() - if self.opts.generate_descriptions: - self.generateThumbnails() + self.generateOPF() self.generateNCXHeader() self.generateNCXByAuthor("Authors") @@ -1452,6 +1452,12 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) self.booksByAuthor = sorted(self.booksByAuthor, key=self.booksByAuthorSorter_author_sort) + +# for book in self.booksByAuthor: +# print '{0:<10} {1:<5} {2:<20} {3:<20} {4:<20} {5:<20}'.format(book['series'], book['series_index'], book['title'], +# book['author'], book['authors'],book['author_sort']) +# print + # Build the unique_authors set from existing data authors = [(record['author'], record['author_sort'].capitalize()) for record in self.booksByAuthor] @@ -2848,23 +2854,26 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) thumb_generated = True valid_cover = True try: - thumbs.append("thumbnail_%d.jpg" % int(title['id'])) self.generateThumbnail(title, image_dir, thumb_file) + thumbs.append("thumbnail_%d.jpg" % int(title['id'])) except: if 'cover' in title and os.path.exists(title['cover']): valid_cover = False - self.opts.log.warn(" *** Invalid cover file for '%s' ***" % (title['title'])) + self.opts.log.warn(" *** Invalid cover file for '%s'***" % + (title['title'])) if not self.error: self.error.append('Invalid cover files') self.error.append("Warning: invalid cover file for '%s', default cover substituted.\n" % (title['title'])) + thumb_generated = False if not thumb_generated: - self.opts.log.warn(" using default cover for '%s'" % (title['title'])) - # Check to make sure default is current - # Check to see if thumbnail exists + self.opts.log.warn(" using default cover for '%s' (%d)" % (title['title'], title['id'])) + # Confirm thumb exists, default is current default_thumb_fp = os.path.join(image_dir,"thumbnail_default.jpg") cover = os.path.join(self.catalogPath, "DefaultCover.png") + title['cover'] = cover + if not os.path.exists(cover): shutil.copyfile(I('book.png'), cover) @@ -2877,17 +2886,15 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) if thumb_timestamp < cover_timestamp: if False and self.verbose: self.opts.log.warn("updating thumbnail_default for %s" % title['title']) - #title['cover'] = os.path.join(self.catalogPath,"DefaultCover.jpg") - title['cover'] = cover self.generateThumbnail(title, image_dir, "thumbnail_default.jpg" if valid_cover else thumb_file) else: if False and self.verbose: self.opts.log.warn(" generating new thumbnail_default.jpg") - #title['cover'] = os.path.join(self.catalogPath,"DefaultCover.jpg") - title['cover'] = cover self.generateThumbnail(title, image_dir, "thumbnail_default.jpg" if valid_cover else thumb_file) + # Clear the book's cover property + title['cover'] = None # Write thumb_width to the file, validating cache contents @@ -3881,7 +3888,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) outfile.write(self.ncxSoup.prettify()) - # --------------- Helpers --------------- + # ======================== Helpers ======================== def author_to_author_sort(self, author): tokens = author.split() tokens = tokens[-1:] + tokens[:-1] @@ -3894,14 +3901,14 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) Sort non-series books before series books ''' if not book['series']: - key = '%s %s' % (book['author_sort'], + key = '%s %s' % (book['author_sort'].capitalize(), book['title_sort'].capitalize()) else: index = book['series_index'] integer = int(index) fraction = index-integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) - key = '%s ~%s %s' % (book['author_sort'], + key = '%s ~%s %s' % (book['author_sort'].capitalize(), self.generateSortTitle(book['series']), series_index) return key @@ -4315,7 +4322,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Thumb _soup = BeautifulSoup('<html>',selfClosingTags=['img']) thumb = Tag(_soup,"img") - if 'cover' in book: + if 'cover' in book and book['cover']: thumb['src'] = "../images/thumbnail_%d.jpg" % int(book['id']) else: thumb['src'] = "../images/thumbnail_default.jpg" From af04ca87cefa556c025e5cecaf5c92934b1264ff Mon Sep 17 00:00:00 2001 From: GRiker <griker@hotmail.com> Date: Mon, 17 Jan 2011 15:52:25 -0700 Subject: [PATCH 35/36] GwR fix for TOC discontinuity, default cover swapping --- src/calibre/library/catalog.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 13df6625d4..8edf266cfb 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -4314,10 +4314,9 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Date of publication pubdate = book['date'] - if pubdate: - pubmonth, pubyear = pubdate.split(' ') - else: - pubmonth = pubyear = '' + pubmonth, pubyear = pubdate.split() + if pubyear == '101': + pubdate = pubmonth = pubyear = '' # Thumb _soup = BeautifulSoup('<html>',selfClosingTags=['img']) From e82dd54242eb86382f55a2840338acea526b4fb1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 17 Jan 2011 17:42:40 -0700 Subject: [PATCH 36/36] Fix #8409 (bulk edit date) --- src/calibre/gui2/dialogs/metadata_bulk.py | 24 ++++- src/calibre/gui2/dialogs/metadata_bulk.ui | 117 +++++++++++++++------- 2 files changed, 100 insertions(+), 41 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 2b3a319663..6e6b553dba 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -49,7 +49,6 @@ def get_cover_data(path): # {{{ return cdata, area # }}} - class MyBlockingBusy(QDialog): # {{{ do_one_signal = pyqtSignal() @@ -134,7 +133,7 @@ class MyBlockingBusy(QDialog): # {{{ do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_remove_conv, do_auto_author, series, do_series_restart, \ series_start_value, do_title_case, cover_action, clear_series, \ - pubdate = self.args + pubdate, adddate = self.args # first loop: do author and title. These will commit at the end of each @@ -214,6 +213,9 @@ class MyBlockingBusy(QDialog): # {{{ if pubdate is not None: self.db.set_pubdate(id, pubdate, notify=False, commit=False) + if adddate is not None: + self.db.set_timestamp(id, adddate, notify=False, commit=False) + if do_series: if do_series_restart: if self.series_start_value is None: @@ -300,6 +302,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.pubdate.setSpecialValueText(_('Undefined')) self.clear_pubdate_button.clicked.connect(self.clear_pubdate) self.pubdate.dateChanged.connect(self.do_apply_pubdate) + self.adddate.setMinimumDate(UNDEFINED_QDATE) + self.adddate.setSpecialValueText(_('Undefined')) + self.clear_adddate_button.clicked.connect(self.clear_adddate) + self.adddate.dateChanged.connect(self.do_apply_adddate) if len(self.db.custom_field_keys(include_composites=False)) == 0: self.central_widget.removeTab(1) @@ -322,6 +328,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): def clear_pubdate(self, *args): self.pubdate.setDate(UNDEFINED_QDATE) + def do_apply_adddate(self, *args): + self.apply_adddate.setChecked(True) + + def clear_adddate(self, *args): + self.adddate.setDate(UNDEFINED_QDATE) + def button_clicked(self, which): if which == self.button_box.button(QDialogButtonBox.Apply): self.do_again = True @@ -726,7 +738,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): name = name.strip().replace('|', ',') self.authors.addItem(name) self.authors.setEditText('') - + self.authors.set_separator('&') self.authors.set_space_before_sep(True) self.authors.update_items_cache(self.db.all_author_names()) @@ -805,9 +817,11 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): do_remove_conv = self.remove_conversion_settings.isChecked() do_auto_author = self.auto_author_sort.isChecked() do_title_case = self.change_title_to_title_case.isChecked() - pubdate = None + pubdate = adddate = None if self.apply_pubdate.isChecked(): pubdate = qt_to_dt(self.pubdate.date()) + if self.apply_adddate.isChecked(): + adddate = qt_to_dt(self.adddate.date()) cover_action = None if self.cover_remove.isChecked(): @@ -821,7 +835,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): do_autonumber, do_remove_format, remove_format, do_swap_ta, do_remove_conv, do_auto_author, series, do_series_restart, series_start_value, do_title_case, cover_action, clear_series, - pubdate) + pubdate, adddate) bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.') %len(self.ids), args, self.db, self.ids, diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 8db74b343d..f8ae926be6 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -347,6 +347,51 @@ from the value in the box</string> </item> </layout> </item> + <item row="9" column="0"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>&Date:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>adddate</cstring> + </property> + </widget> + </item> + <item row="9" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="QDateEdit" name="adddate"> + <property name="displayFormat"> + <string>d MMM yyyy</string> + </property> + <property name="calendarPopup"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="clear_adddate_button"> + <property name="text"> + <string>...</string> + </property> + <property name="icon"> + <iconset resource="../../../../resources/images.qrc"> + <normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="9" column="2"> + <widget class="QCheckBox" name="apply_adddate"> + <property name="text"> + <string>&Apply date</string> + </property> + </widget> + </item> <item row="10" column="0"> <widget class="QLabel" name="label_9"> <property name="text"> @@ -395,6 +440,42 @@ from the value in the box</string> </property> </widget> </item> + <item row="11" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Remove &format:</string> + </property> + <property name="buddy"> + <cstring>remove_format</cstring> + </property> + </widget> + </item> + <item row="11" column="1"> + <widget class="QComboBox" name="remove_format"> + <property name="maximumSize"> + <size> + <width>120</width> + <height>16777215</height> + </size> + </property> + </widget> + </item> + <item row="12" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>15</height> + </size> + </property> + </spacer> + </item> <item row="13" column="0" colspan="3"> <layout class="QHBoxLayout" name="horizontalLayout_3"> <item> @@ -478,42 +559,6 @@ Future conversion of these books will use the default settings.</string> </property> </spacer> </item> - <item row="12" column="0"> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>15</height> - </size> - </property> - </spacer> - </item> - <item row="11" column="0"> - <widget class="QLabel" name="label_5"> - <property name="text"> - <string>Remove &format:</string> - </property> - <property name="buddy"> - <cstring>remove_format</cstring> - </property> - </widget> - </item> - <item row="11" column="1"> - <widget class="QComboBox" name="remove_format"> - <property name="maximumSize"> - <size> - <width>120</width> - <height>16777215</height> - </size> - </property> - </widget> - </item> </layout> </widget> <widget class="QWidget" name="tab">