From 8bd3c8d53e0d7d14784fdc9027cd02dd32564b03 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 26 Jan 2011 20:15:44 -0500 Subject: [PATCH 01/32] TXT Input: Detect and retain soft scene breaks. --- src/calibre/ebooks/txt/processor.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/txt/processor.py b/src/calibre/ebooks/txt/processor.py index 43aadc6576..9d6868467e 100644 --- a/src/calibre/ebooks/txt/processor.py +++ b/src/calibre/ebooks/txt/processor.py @@ -31,7 +31,7 @@ def clean_txt(txt): txt = re.sub('^\s+(?=.)', '', txt) txt = re.sub('(?<=.)\s+$', '', txt) # Remove excessive line breaks. - txt = re.sub('\n{3,}', '\n\n', txt) + txt = re.sub('\n{5,}', '\n\n\n\n', txt) #remove ASCII invalid chars : 0 to 8 and 11-14 to 24 txt = clean_ascii_chars(txt) @@ -59,10 +59,16 @@ def convert_basic(txt, title='', epub_split_size_kb=0): txt = split_txt(txt, epub_split_size_kb) lines = [] + blank_count = 0 # Split into paragraphs based on having a blank line between text. - for line in txt.split('\n\n'): + for line in txt.split('\n'): if line.strip(): + blank_count = 0 lines.append(u'

%s

' % prepare_string_for_xml(line.replace('\n', ' '))) + else: + blank_count += 1 + if blank_count == 2: + lines.append(u'

 

') return HTML_TEMPLATE % (title, u'\n'.join(lines)) @@ -85,7 +91,8 @@ def normalize_line_endings(txt): return txt def separate_paragraphs_single_line(txt): - txt = re.sub(u'(?<=.)\n(?=.)', '\n\n', txt) + #txt = re.sub(u'(?<=.)\n(?=.)', '\n\n', txt) + txt = txt.replace('\n', '\n\n') return txt def separate_paragraphs_print_formatted(txt): From b3ec6480600acb0c701df9fc309abb37d6e3fa81 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 26 Jan 2011 20:17:09 -0500 Subject: [PATCH 02/32] TXT Input: Don't preserve spaces in heuristic processing --- src/calibre/ebooks/txt/input.py | 1 - src/calibre/ebooks/txt/processor.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py index 6ec1edb65c..2399e599ae 100644 --- a/src/calibre/ebooks/txt/input.py +++ b/src/calibre/ebooks/txt/input.py @@ -83,7 +83,6 @@ class TXTInput(InputFormatPlugin): setattr(options, 'markup_chapter_headings', True) setattr(options, 'italicize_common_cases', True) setattr(options, 'fix_indents', True) - setattr(options, 'preserve_spaces', True) setattr(options, 'delete_blank_paragraphs', True) setattr(options, 'format_scene_breaks', True) setattr(options, 'dehyphenate', True) diff --git a/src/calibre/ebooks/txt/processor.py b/src/calibre/ebooks/txt/processor.py index 9d6868467e..926e9a8dd6 100644 --- a/src/calibre/ebooks/txt/processor.py +++ b/src/calibre/ebooks/txt/processor.py @@ -91,7 +91,6 @@ def normalize_line_endings(txt): return txt def separate_paragraphs_single_line(txt): - #txt = re.sub(u'(?<=.)\n(?=.)', '\n\n', txt) txt = txt.replace('\n', '\n\n') return txt From 46c27899276eaf06e837cfa161c0ecf2fe334e78 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 27 Jan 2011 08:42:14 +0000 Subject: [PATCH 03/32] Better integrate right-click search into the existing toggle-node structure --- src/calibre/gui2/tag_view.py | 57 +++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index d7b9c62ed1..677ebac083 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -64,6 +64,8 @@ class TagDelegate(QItemDelegate): # {{{ # }}} +TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_minus': 2} + class TagsView(QTreeView): # {{{ refresh_required = pyqtSignal() @@ -177,9 +179,16 @@ class TagsView(QTreeView): # {{{ return joiner.join(tokens) def toggle(self, index): + self._toggle(index, None) + + def _toggle(self, index, set_to): + ''' + set_to: if None, advance the state. Otherwise must be one of the values + in TAG_SEARCH_STATES + ''' modifiers = int(QApplication.keyboardModifiers()) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) - if self._model.toggle(index, exclusive): + if self._model.toggle(index, exclusive, set_to=set_to): self.tags_marked.emit(self.search_string) def conditional_clear(self, search_string): @@ -187,7 +196,7 @@ class TagsView(QTreeView): # {{{ self.clear() def context_menu_handler(self, action=None, category=None, - key=None, index=None, negate=None): + key=None, index=None, search_state=None): if not action: return try: @@ -201,11 +210,10 @@ class TagsView(QTreeView): # {{{ self.user_category_edit.emit(category) return if action == 'search': - self.tags_marked.emit(('not ' if negate else '') + - category + ':"=' + key + '"') + self._toggle(index, set_to=search_state) return if action == 'search_category': - self.tags_marked.emit(category + ':' + str(not negate)) + self.tags_marked.emit(key + ':' + search_state) return if action == 'manage_searches': self.saved_search_edit.emit(category) @@ -270,20 +278,16 @@ class TagsView(QTreeView): # {{{ partial(self.context_menu_handler, action='edit_author_sort', index=tag_id)) # Add the search for value items - n = tag_name - c = category - if self.db.field_metadata[key]['datatype'] == 'rating': - n = str(len(tag_name)) - elif self.db.field_metadata[key]['kind'] in ['user', 'search']: - c = tag_item.tag.category self.context_menu.addAction(self.search_icon, _('Search for %s')%tag_name, partial(self.context_menu_handler, action='search', - category=c, key=n, negate=False)) + search_state=TAG_SEARCH_STATES['mark_plus'], + index=index)) self.context_menu.addAction(self.search_icon, _('Search for everything but %s')%tag_name, partial(self.context_menu_handler, action='search', - category=c, key=n, negate=True)) + search_state=TAG_SEARCH_STATES['mark_minus'], + index=index)) self.context_menu.addSeparator() # Hide/Show/Restore categories self.context_menu.addAction(_('Hide category %s') % category, @@ -299,11 +303,11 @@ class TagsView(QTreeView): # {{{ self.context_menu.addAction(self.search_icon, _('Search for books in category %s')%category, partial(self.context_menu_handler, action='search_category', - category=key, negate=False)) + key=key, search_state='true')) self.context_menu.addAction(self.search_icon, _('Search for books not in category %s')%category, partial(self.context_menu_handler, action='search_category', - category=key, negate=True)) + key=key, search_state='false')) # Offer specific editors for tags/series/publishers/saved searches self.context_menu.addSeparator() if key in ['tags', 'publisher', 'series'] or \ @@ -528,9 +532,15 @@ class TagTreeItem(object): # {{{ return QVariant(self.tooltip) return NONE - def toggle(self): + def toggle(self, set_to=None): + ''' + set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES + ''' if self.type == self.TAG: - self.tag.state = (self.tag.state + 1)%3 + if set_to is None: + self.tag.state = (self.tag.state + 1)%3 + else: + self.tag.state = set_to def child_tags(self): res = [] @@ -1014,11 +1024,15 @@ class TagsModel(QAbstractItemModel): # {{{ def clear_state(self): self.reset_all_states() - def toggle(self, index, exclusive): + def toggle(self, index, exclusive, set_to=None): + ''' + exclusive: clear all states before applying this one + set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES + ''' if not index.isValid(): return False item = index.internalPointer() if item.type == TagTreeItem.TAG: - item.toggle() + item.toggle(set_to=set_to) if exclusive: self.reset_all_states(except_=item.tag) self.dataChanged.emit(index, index) @@ -1040,8 +1054,9 @@ class TagsModel(QAbstractItemModel): # {{{ category_item = self.root_item.children[row_index] for tag_item in category_item.child_tags(): tag = tag_item.tag - if tag.state > 0: - prefix = ' not ' if tag.state == 2 else '' + if tag.state != TAG_SEARCH_STATES['clear']: + prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \ + else '' category = key if key != 'news' else 'tag' if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating ans.append('%s%s:%s'%(prefix, category, len(tag.name))) From ceea62626450d93dfaf77946538ccb827aec92a2 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 27 Jan 2011 08:50:50 +0000 Subject: [PATCH 04/32] 1) clean up commented print statements 2) change an incorrect comment in caches.py --- src/calibre/library/caches.py | 2 +- src/calibre/library/database2.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 67529397b3..dd4509acea 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -49,7 +49,7 @@ class MetadataBackup(Thread): # {{{ def run(self): while self.keep_running: try: - time.sleep(2) # Limit to two per second + time.sleep(2) # Limit to one book per two seconds (id_, sequence) = self.db.get_a_dirtied_book() if id_ is None: continue diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index e1a1adc4ff..da1797bff8 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -618,9 +618,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ''' with self.dirtied_lock: dc_sequence = self.dirtied_cache.get(book_id, None) -# print 'clear_dirty: check book', book_id, dc_sequence + # print 'clear_dirty: check book', book_id, dc_sequence if dc_sequence is None or sequence is None or dc_sequence == sequence: -# print 'needs to be cleaned' + # print 'needs to be cleaned' self.conn.execute('DELETE FROM metadata_dirtied WHERE book=?', (book_id,)) self.conn.commit() @@ -629,7 +629,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): except: pass elif dc_sequence is not None: -# print 'book needs to be done again' + # print 'book needs to be done again' pass def dump_metadata(self, book_ids=None, remove_from_dirtied=True, @@ -661,12 +661,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): changed = False for book in book_ids: with self.dirtied_lock: -# print 'dirtied: check id', book + # print 'dirtied: check id', book if book in self.dirtied_cache: self.dirtied_cache[book] = self.dirtied_sequence self.dirtied_sequence += 1 continue -# print 'book not already dirty' + # print 'book not already dirty' try: self.conn.execute( 'INSERT INTO metadata_dirtied (book) VALUES (?)', @@ -720,7 +720,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # thread has not done the work between the put and the get_metadata with self.dirtied_lock: sequence = self.dirtied_cache.get(idx, None) -# print 'get_md_for_dump', idx, sequence + # print 'get_md_for_dump', idx, sequence try: # While a book is being created, the path is empty. Don't bother to # try to write the opf, because it will go to the wrong folder. @@ -827,7 +827,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): try: book_ids = self.data.parse(query) except: - import traceback traceback.print_exc() return identical_book_ids for book_id in book_ids: From ff2c0f0d2c3da8941db1c5d508703a5f813ada7a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 27 Jan 2011 13:15:35 +0000 Subject: [PATCH 05/32] Refactor bulk custom widgets to add an 'apply changes' checkbox to each widget --- src/calibre/gui2/custom_column_widgets.py | 311 ++++++++++++++++------ 1 file changed, 227 insertions(+), 84 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index c873d1ed94..360a5bcd0a 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -151,12 +151,27 @@ class DateEdit(QDateEdit): def set_to_today(self): self.setDate(now()) + def set_to_clear(self): + self.setDate(UNDEFINED_QDATE) + class DateTime(Base): def setup_ui(self, parent): cm = self.col_metadata - self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent), - QLabel(''), QPushButton(_('Set \'%s\' to today')%cm['name'], parent)] + self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent)] + self.widgets.append(QLabel('')) + w = QWidget(parent) + self.widgets.append(w) + l = QHBoxLayout() + l.setContentsMargins(0, 0, 0, 0) + w.setLayout(l) + l.addStretch(1) + self.today_button = QPushButton(_('Set \'%s\' to today')%cm['name'], parent) + l.addWidget(self.today_button) + self.clear_button = QPushButton(_('Clear \'%s\'')%cm['name'], parent) + l.addWidget(self.clear_button) + l.addStretch(2) + w = self.widgets[1] format = cm['display'].get('date_format','') if not format: @@ -165,7 +180,8 @@ class DateTime(Base): w.setCalendarPopup(True) w.setMinimumDate(UNDEFINED_QDATE) w.setSpecialValueText(_('Undefined')) - self.widgets[3].clicked.connect(w.set_to_today) + self.today_button.clicked.connect(w.set_to_today) + self.clear_button.clicked.connect(w.set_to_clear) def setter(self, val): if val is None: @@ -470,11 +486,48 @@ class BulkBase(Base): self.setter(val) def commit(self, book_ids, notify=False): + if not self.a_c_checkbox.isChecked(): + return val = self.gui_val val = self.normalize_ui_val(val) if val != self.initial_val: self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) + def make_widgets(self, parent, main_widget_class, extra_label_text=''): + w = QWidget(parent) + self.widgets = [QLabel('&'+self.col_metadata['name']+':', w), w] + l = QHBoxLayout() + l.setContentsMargins(0, 0, 0, 0) + w.setLayout(l) + self.main_widget = main_widget_class(w) + l.addWidget(self.main_widget) + l.setStretchFactor(self.main_widget, 10) + self.a_c_checkbox = QCheckBox( _('Apply changes'), w) + l.addWidget(self.a_c_checkbox) + self.ignore_change_signals = True + + # connect to the various changed signals so we can auto-update the + # apply changes checkbox + if hasattr(self.main_widget, 'editTextChanged'): + # editable combobox widgets + self.main_widget.editTextChanged.connect(self.a_c_checkbox_changed) + if hasattr(self.main_widget, 'textChanged'): + # lineEdit widgets + self.main_widget.textChanged.connect(self.a_c_checkbox_changed) + if hasattr(self.main_widget, 'currentIndexChanged'): + # combobox widgets + self.main_widget.currentIndexChanged[int].connect(self.a_c_checkbox_changed) + if hasattr(self.main_widget, 'valueChanged'): + # spinbox widgets + self.main_widget.valueChanged.connect(self.a_c_checkbox_changed) + if hasattr(self.main_widget, 'dateChanged'): + # dateEdit widgets + self.main_widget.dateChanged.connect(self.a_c_checkbox_changed) + + def a_c_checkbox_changed(self): + if not self.ignore_change_signals: + self.a_c_checkbox.setChecked(True) + class BulkBool(BulkBase, Bool): def get_initial_value(self, book_ids): @@ -484,58 +537,144 @@ class BulkBool(BulkBase, Bool): if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: val = False if value is not None and value != val: - return 'nochange' + return None value = val return value def setup_ui(self, parent): - self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), - QComboBox(parent)] - w = self.widgets[1] - items = [_('Yes'), _('No'), _('Undefined'), _('Do not change')] - icons = [I('ok.png'), I('list_remove.png'), I('blank.png'), I('blank.png')] + self.make_widgets(parent, QComboBox) + items = [_('Yes'), _('No'), _('Undefined')] + icons = [I('ok.png'), I('list_remove.png'), I('blank.png')] + self.main_widget.blockSignals(True) for icon, text in zip(icons, items): - w.addItem(QIcon(icon), text) + self.main_widget.addItem(QIcon(icon), text) + self.main_widget.blockSignals(False) def getter(self): - val = self.widgets[1].currentIndex() - return {3: 'nochange', 2: None, 1: False, 0: True}[val] + val = self.main_widget.currentIndex() + return {2: None, 1: False, 0: True}[val] def setter(self, val): - val = {'nochange': 3, None: 2, False: 1, True: 0}[val] - self.widgets[1].setCurrentIndex(val) + val = {None: 2, False: 1, True: 0}[val] + self.main_widget.setCurrentIndex(val) + self.ignore_change_signals = False def commit(self, book_ids, notify=False): + if not self.a_c_checkbox.isChecked(): + return val = self.gui_val val = self.normalize_ui_val(val) - if val != self.initial_val and val != 'nochange': - if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: - val = False - self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) + if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: + val = False + self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) -class BulkInt(BulkBase, Int): - pass +class BulkInt(BulkBase): -class BulkFloat(BulkBase, Float): - pass + def setup_ui(self, parent): + self.make_widgets(parent, QSpinBox) + self.main_widget.setRange(-100, sys.maxint) + self.main_widget.setSpecialValueText(_('Undefined')) + self.main_widget.setSingleStep(1) -class BulkRating(BulkBase, Rating): - pass + def setter(self, val): + if val is None: + val = self.main_widget.minimum() + else: + val = int(val) + self.main_widget.setValue(val) + self.ignore_change_signals = False -class BulkDateTime(BulkBase, DateTime): - pass + def getter(self): + val = self.main_widget.value() + if val == self.main_widget.minimum(): + val = None + return val + +class BulkFloat(BulkInt): + + def setup_ui(self, parent): + self.make_widgets(parent, QDoubleSpinBox) + self.main_widget.setRange(-100., float(sys.maxint)) + self.main_widget.setDecimals(2) + self.main_widget.setSpecialValueText(_('Undefined')) + self.main_widget.setSingleStep(1) + +class BulkRating(BulkBase): + + def setup_ui(self, parent): + self.make_widgets(parent, QSpinBox) + self.main_widget.setRange(0, 5) + self.main_widget.setSuffix(' '+_('star(s)')) + self.main_widget.setSpecialValueText(_('Unrated')) + self.main_widget.setSingleStep(1) + + def setter(self, val): + if val is None: + val = 0 + self.main_widget.setValue(int(round(val/2.))) + self.ignore_change_signals = False + + def getter(self): + val = self.main_widget.value() + if val == 0: + val = None + else: + val *= 2 + return val + +class BulkDateTime(BulkBase): + + def setup_ui(self, parent): + cm = self.col_metadata + self.make_widgets(parent, DateEdit) + self.widgets.append(QLabel('')) + w = QWidget(parent) + self.widgets.append(w) + l = QHBoxLayout() + l.setContentsMargins(0, 0, 0, 0) + w.setLayout(l) + l.addStretch(1) + self.today_button = QPushButton(_('Set \'%s\' to today')%cm['name'], parent) + l.addWidget(self.today_button) + self.clear_button = QPushButton(_('Clear \'%s\'')%cm['name'], parent) + l.addWidget(self.clear_button) + l.addStretch(2) + + w = self.main_widget + format = cm['display'].get('date_format','') + if not format: + format = 'dd MMM yyyy' + w.setDisplayFormat(format) + w.setCalendarPopup(True) + w.setMinimumDate(UNDEFINED_QDATE) + w.setSpecialValueText(_('Undefined')) + self.today_button.clicked.connect(w.set_to_today) + self.clear_button.clicked.connect(w.set_to_clear) + + def setter(self, val): + if val is None: + val = self.main_widget.minimumDate() + else: + val = QDate(val.year, val.month, val.day) + self.main_widget.setDate(val) + self.ignore_change_signals = False + + def getter(self): + val = self.main_widget.date() + if val == UNDEFINED_QDATE: + val = None + else: + val = qt_to_dt(val) + return val class BulkSeries(BulkBase): def setup_ui(self, parent): + self.make_widgets(parent, EnComboBox) values = self.all_values = list(self.db.all_custom(num=self.col_id)) values.sort(key=sort_key) - w = EnComboBox(parent) - w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) - w.setMinimumContentsLength(25) - self.name_widget = w - self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w] - + self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon) + self.main_widget.setMinimumContentsLength(25) self.widgets.append(QLabel('', parent)) w = QWidget(parent) layout = QHBoxLayout(w) @@ -555,15 +694,24 @@ class BulkSeries(BulkBase): layout.addWidget(self.series_start_number) layout.addItem(QSpacerItem(20, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) self.widgets.append(w) + self.idx_widget.stateChanged.connect(self.check_changed_checkbox) + self.force_number.stateChanged.connect(self.check_changed_checkbox) + self.series_start_number.valueChanged.connect(self.check_changed_checkbox) + self.remove_series.stateChanged.connect(self.check_changed_checkbox) + self.ignore_change_signals = False + + def check_changed_checkbox(self): + self.a_c_checkbox.setChecked(True) def initialize(self, book_id): self.idx_widget.setChecked(False) for c in self.all_values: - self.name_widget.addItem(c) - self.name_widget.setEditText('') + self.main_widget.addItem(c) + self.main_widget.setEditText('') + self.a_c_checkbox.setChecked(False) def getter(self): - n = unicode(self.name_widget.currentText()).strip() + n = unicode(self.main_widget.currentText()).strip() i = self.idx_widget.checkState() f = self.force_number.checkState() s = self.series_start_number.value() @@ -571,6 +719,8 @@ class BulkSeries(BulkBase): return n, i, f, s, r def commit(self, book_ids, notify=False): + if not self.a_c_checkbox.isChecked(): + return val, update_indices, force_start, at_value, clear = self.gui_val val = None if clear else self.normalize_ui_val(val) if clear or val != '': @@ -598,9 +748,9 @@ class BulkEnumeration(BulkBase, Enumeration): def get_initial_value(self, book_ids): value = None - ret_value = None + first = True dialog_shown = False - for i,book_id in enumerate(book_ids): + for book_id in book_ids: val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) if val and val not in self.col_metadata['display']['enum_values']: if not dialog_shown: @@ -610,44 +760,32 @@ class BulkEnumeration(BulkBase, Enumeration): self.col_metadata['name']), show=True, show_copy_button=False) dialog_shown = True - ret_value = ' nochange ' - elif (value is not None and value != val) or (val and i != 0): - ret_value = ' nochange ' - value = val - if ret_value is None: - return value - return ret_value + if first: + value = val + first = False + elif value != val: + value = None + if not value: + self.ignore_change_signals = False + return value def setup_ui(self, parent): - self.parent = parent - self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), - QComboBox(parent)] - w = self.widgets[1] + self.make_widgets(parent, QComboBox) vals = self.col_metadata['display']['enum_values'] - w.addItem('Do Not Change') - w.addItem('') - for v in vals: - w.addItem(v) + self.main_widget.blockSignals(True) + self.main_widget.addItem('') + self.main_widget.addItems(vals) + self.main_widget.blockSignals(False) def getter(self): - if self.widgets[1].currentIndex() == 0: - return ' nochange ' - return unicode(self.widgets[1].currentText()) + return unicode(self.main_widget.currentText()) def setter(self, val): - if val == ' nochange ': - self.widgets[1].setCurrentIndex(0) + if val is None: + self.main_widget.setCurrentIndex(0) else: - if val is None: - self.widgets[1].setCurrentIndex(1) - else: - self.widgets[1].setCurrentIndex(self.widgets[1].findText(val)) - - def commit(self, book_ids, notify=False): - val = self.gui_val - val = self.normalize_ui_val(val) - if val != self.initial_val and val != ' nochange ': - self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) + self.main_widget.setCurrentIndex(self.main_widget.findText(val)) + self.ignore_change_signals = False class RemoveTags(QWidget): @@ -658,11 +796,10 @@ class RemoveTags(QWidget): layout.setContentsMargins(0, 0, 0, 0) self.tags_box = CompleteLineEdit(parent, values) - layout.addWidget(self.tags_box, stretch = 1) - # self.tags_box.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - + layout.addWidget(self.tags_box, stretch=3) self.checkbox = QCheckBox(_('Remove all tags'), parent) layout.addWidget(self.checkbox) + layout.addStretch(1) self.setLayout(layout) self.connect(self.checkbox, SIGNAL('stateChanged(int)'), self.box_touched) @@ -679,39 +816,45 @@ 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 = CompleteLineEdit(parent, values) - w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - self.widgets = [QLabel('&'+self.col_metadata['name']+': ' + - _('tags to add'), parent), w] - self.adding_widget = w + self.make_widgets(parent, CompleteLineEdit, + extra_label_text=_('tags to add')) + self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + self.adding_widget = self.main_widget w = RemoveTags(parent, values) self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' + _('tags to remove'), parent)) self.widgets.append(w) self.removing_widget = w + w.tags_box.textChanged.connect(self.a_c_checkbox_changed) + w.checkbox.stateChanged.connect(self.a_c_checkbox_changed) else: - w = EnComboBox(parent) - w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) - w.setMinimumContentsLength(25) - self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w] + self.make_widgets(parent, EnComboBox) + self.main_widget.setSizeAdjustPolicy( + self.main_widget.AdjustToMinimumContentsLengthWithIcon) + self.main_widget.setMinimumContentsLength(25) + self.ignore_change_signals = False def initialize(self, book_ids): if self.col_metadata['is_multiple']: - self.widgets[1].update_items_cache(self.all_values) + self.main_widget.update_items_cache(self.all_values) else: val = self.get_initial_value(book_ids) self.initial_val = val = self.normalize_db_val(val) idx = None + self.main_widget.blockSignals(True) for i, c in enumerate(self.all_values): if c == val: idx = i - self.widgets[1].addItem(c) - self.widgets[1].setEditText('') + self.main_widget.addItem(c) + self.main_widget.setEditText('') if idx is not None: - self.widgets[1].setCurrentIndex(idx) + self.main_widget.setCurrentIndex(idx) + self.main_widget.blockSignals(False) def commit(self, book_ids, notify=False): + if not self.a_c_checkbox.isChecked(): + return if self.col_metadata['is_multiple']: remove_all, adding, rtext = self.gui_val remove = set() @@ -740,7 +883,7 @@ class BulkText(BulkBase): unicode(self.adding_widget.text()), \ unicode(self.removing_widget.tags_box.text()) - val = unicode(self.widgets[1].currentText()).strip() + val = unicode(self.main_widget.currentText()).strip() if not val: val = None return val From 601eff81cd485986cb22c7e18cccfdca1c91ea01 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 08:41:16 -0700 Subject: [PATCH 06/32] La Nueva Espana by Luis Hernandez --- resources/recipes/la_nueva.recipe | 74 +++++++++++++++++++++++++++++ resources/recipes/la_tribuna.recipe | 26 +++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 resources/recipes/la_nueva.recipe diff --git a/resources/recipes/la_nueva.recipe b/resources/recipes/la_nueva.recipe new file mode 100644 index 0000000000..84b7118cf6 --- /dev/null +++ b/resources/recipes/la_nueva.recipe @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +__license__ = 'GPL v3' +__author__ = 'Luis Hernandez' +__copyright__ = 'Luis Hernandez' +description = 'Diario independiente de Asturias - v1.0 - 27 Jan 2011' + +''' +www.lne.es +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1294946868(BasicNewsRecipe): + + title = u'La Nueva España' + publisher = u'Editorial Prensa Iberica' + + __author__ = 'Luis Hernandez' + description = 'Diario independiente de Asturias' + cover_url = 'http://estaticos00.lne.es//elementosWeb/mediaweb/images/iconos/logo2.jpg' + + oldest_article = 3 + max_articles_per_feed = 100 + + remove_javascript = True + no_stylesheets = True + use_embedded_content = False + + encoding = 'ISO-8859-1' + language = 'es' + timefmt = '[%a, %d %b, %Y]' + + keep_only_tags = [ + dict(name='div', attrs={'class':['noticia_titular','subtitulo','noticiadd2','noticia_texto']}) + ,dict(name='div', attrs={'id':['noticia_texto']}) + ] + + extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h1{ font-family: sans-serif; font-size:150%; font-weight: 600; text-align: justify; } h2{ font-family: sans-serif; font-size:120%; font-weight: 500; text-align: justify } ' + + + remove_tags_before = dict(name='div' , attrs={'class':['contenedor']}) + remove_tags_after = dict(name='div' , attrs={'class':['fin_noticia']}) + + remove_tags = [ + dict(name='div', attrs={'class':['epigrafe','antetitulo','bloqueclear','bloqueclear_video','cuadro_multimedia','cintillo2','editor_documentos','noticiadd','noticiadd3','noticiainterior','fin_noticia']}) + ,dict(name='div', attrs={'id':['evotos']}) + ] + + feeds = [ + (u'Al minuto' , u'http://www.lne.es/elementosInt/rss/AlMinuto') + ,(u'General' , u'http://www.lne.es/elementosInt/rss/55') + ,(u'Nacional' , u'http://www.lne.es/elementosInt/rss/43') + ,(u'Internacional' , u'http://www.lne.es/elementosInt/rss/44') + ,(u'Economia' , u'http://www.lne.es/elementosInt/rss/45') + ,(u'Deportes' , u'http://www.lne.es/elementosInt/rss/47') + ,(u'Campeones' , u'http://www.lne.es/elementosInt/rss/65') + ,(u'Sociedad' , u'http://www.lne.es/elementosInt/rss/46') + ,(u'Sucesos' , u'http://www.lne.es/elementosInt/rss/48') + ,(u'Galeria' , u'http://www.lne.es/elementosInt/rss/51') + ,(u'Cultura' , u'http://www.lne.es/elementosInt/rss/66') + ,(u'Motor' , u'http://www.lne.es/elementosInt/rss/62') + ,(u'Opinion' , u'http://www.lne.es/elementosInt/rss/52') + ,(u'Asturias' , u'http://www.lne.es/elementosInt/rss/42') + ,(u'Oviedo' , u'http://www.lne.es/elementosInt/rss/31') + ,(u'Gijon' , u'http://www.lne.es/elementosInt/rss/35') + ,(u'Aviles' , u'http://www.lne.es/elementosInt/rss/36') + ,(u'Nalon' , u'http://www.lne.es/elementosInt/rss/37') + ,(u'Cuencas' , u'http://www.lne.es/elementosInt/rss/38') + ,(u'Caudal' , u'http://www.lne.es/elementosInt/rss/39') + ,(u'Oriente' , u'http://www.lne.es/elementosInt/rss/40') + ,(u'Occidente' , u'http://www.lne.es/elementosInt/rss/41') + ,(u'Mar y Campo' , u'http://www.lne.es/elementosInt/rss/63') + ,(u'Ultima' , u'http://www.lne.es/elementosInt/rss/50') +] diff --git a/resources/recipes/la_tribuna.recipe b/resources/recipes/la_tribuna.recipe index 11bdda8f3e..739d11cc8d 100644 --- a/resources/recipes/la_tribuna.recipe +++ b/resources/recipes/la_tribuna.recipe @@ -1,9 +1,22 @@ +# -*- coding: utf-8 -*- +__license__ = 'GPL v3' +__author__ = 'Luis Hernandez' +__copyright__ = 'Luis Hernandez' +description = 'Diario local de Talavera de la Reina - v1.2 - 27 Jan 2011' + +''' +http://www.latribunadetalavera.es/ +''' + from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1294946868(BasicNewsRecipe): + title = u'La Tribuna de Talavera' + publisher = u'Grupo PROMECAL' + __author__ = 'Luis Hernández' - description = 'Diario de Talavera de la Reina' + description = 'Diario local de Talavera de la Reina' cover_url = 'http://www.latribunadetalavera.es/entorno/mancheta.gif' oldest_article = 5 @@ -17,7 +30,8 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe): language = 'es' timefmt = '[%a, %d %b, %Y]' - keep_only_tags = [dict(name='div', attrs={'id':['articulo']}) + keep_only_tags = [ + dict(name='div', attrs={'id':['articulo']}) ,dict(name='div', attrs={'class':['foto']}) ,dict(name='p', attrs={'id':['texto']}) ] @@ -25,5 +39,13 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe): remove_tags_before = dict(name='div' , attrs={'class':['comparte']}) remove_tags_after = dict(name='div' , attrs={'id':['relacionadas']}) + extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h1{ font-family: sans-serif; font-size:150%; font-weight: 700; text-align: justify; } h2{ font-family: sans-serif; font-size:120%; font-weight: 600; text-align: justify } h3{ font-family: sans-serif; font-size:60%; font-weight: 600; text-align: left } h4{ font-family: sans-serif; font-size:80%; font-weight: 600; text-align: left } h5{ font-family: sans-serif; font-size:70%; font-weight: 600; text-align: left }img{margin-bottom: 0.4em} ' + + def preprocess_html(self, soup): + for alink in soup.findAll('a'): + if alink.string is not None: + tstr = alink.string + alink.replaceWith(tstr) + return soup feeds = [(u'Portada', u'http://www.latribunadetalavera.es/rss.html')] From 8749a069de66be89ab371f462be4fb029217e5e6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 09:14:06 -0700 Subject: [PATCH 07/32] ... --- resources/recipes/20_minutos.recipe | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/resources/recipes/20_minutos.recipe b/resources/recipes/20_minutos.recipe index 1f862847dc..cb3002a76c 100644 --- a/resources/recipes/20_minutos.recipe +++ b/resources/recipes/20_minutos.recipe @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __author__ = 'Luis Hernandez' __copyright__ = 'Luis Hernandez' -description = 'Periódico gratuito en español - v0.5 - 25 Jan 2011' +description = 'Periódico gratuito en español - v0.8 - 27 Jan 2011' ''' www.20minutos.es @@ -15,8 +15,8 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe): title = u'20 Minutos' publisher = u'Grupo 20 Minutos' - __author__ = u'Luis Hernández' - description = u'Periódico gratuito en español' + __author__ = 'Luis Hernández' + description = 'Periódico gratuito en español' cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif' oldest_article = 5 @@ -30,8 +30,9 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe): language = 'es' timefmt = '[%a, %d %b, %Y]' - keep_only_tags = [dict(name='div', attrs={'id':['content']}) - ,dict(name='div', attrs={'class':['boxed','description','lead','article-content']}) + keep_only_tags = [ + dict(name='div', attrs={'id':['content','vinetas',]}) + ,dict(name='div', attrs={'class':['boxed','description','lead','article-content','cuerpo estirar']}) ,dict(name='span', attrs={'class':['photo-bar']}) ,dict(name='ul', attrs={'class':['article-author']}) ] @@ -42,10 +43,12 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe): remove_tags = [ dict(name='ol', attrs={'class':['navigation',]}) ,dict(name='span', attrs={'class':['action']}) - ,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col']}) - ,dict(name='div', attrs={'id':['twitter-destacados']}) + ,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col','photo-gallery','calendario','article-comment','postto estirar','otras_vinetas estirar','kment','user-actions']}) + ,dict(name='div', attrs={'id':['twitter-destacados','eco-tabs','inner','vineta_calendario','vinetistas clearfix','otras_vinetas estirar','MIN1','main','SUP1','INT']}) ,dict(name='ul', attrs={'class':['article-user-actions','stripped-list']}) - ] + ,dict(name='ul', attrs={'id':['site-links']}) + ,dict(name='li', attrs={'class':['puntuacion','enviar','compartir']}) + ] feeds = [ (u'Portada' , u'http://www.20minutos.es/rss/') @@ -62,6 +65,6 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe): ,(u'Empleo' , u'http://www.20minutos.es/rss/empleo/') ,(u'Cine' , u'http://www.20minutos.es/rss/cine/') ,(u'Musica' , u'http://www.20minutos.es/rss/musica/') + ,(u'Vinetas' , u'http://www.20minutos.es/rss/vinetas/') ,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/') ] - From f050c0c2250a533d96b94fc05f287bc7de811de6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 09:20:43 -0700 Subject: [PATCH 08/32] David Bravo's Blog by Luis Hernandez --- resources/recipes/dbb.recipe | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 resources/recipes/dbb.recipe diff --git a/resources/recipes/dbb.recipe b/resources/recipes/dbb.recipe new file mode 100644 index 0000000000..d137e78f7a --- /dev/null +++ b/resources/recipes/dbb.recipe @@ -0,0 +1,45 @@ +# -*- coding: utf-8 + +__license__ = 'GPL v3' +__author__ = 'Luis Hernandez' +__copyright__ = 'Luis Hernandez' + +''' +http://www.filmica.com/david_bravo/ +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1294946868(BasicNewsRecipe): + + title = u'Blog de David Bravo' + publisher = u'Filmica' + + __author__ = 'Luis Hernández' + description = 'blog sobre leyes, p2p y copyright' + cover_url = 'http://www.elpais.es/edigitales/image.php?foto=par/portada/1551.jpg' + + oldest_article = 365 + max_articles_per_feed = 100 + + remove_javascript = True + no_stylesheets = True + use_embedded_content = False + + encoding = 'ISO-8859-1' + language = 'es' + timefmt = '[%a, %d %b, %Y]' + + keep_only_tags = [ + dict(name='div', attrs={'class':['blog','date','blogbody','comments-head','comments-body']}) + ,dict(name='span', attrs={'class':['comments-post']}) + ] + + remove_tags_before = dict(name='div' , attrs={'id':['bitacoras']}) + remove_tags_after = dict(name='div' , attrs={'id':['comments-body']}) + + extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h2{ font-family: sans-serif; font-size:75%; font-weight: 800; text-align: justify } h3{ font-family: sans-serif; font-size:150%; font-weight: 600; text-align: left } img{margin-bottom: 0.4em} ' + + + + feeds = [(u'Blog', u'http://www.filmica.com/david_bravo/index.rdf')] From 7376b040696328b8dfec58577fa4f235b6546408 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 09:37:51 -0700 Subject: [PATCH 09/32] Create catalog dialog now remembers its size --- src/calibre/gui2/actions/catalog.py | 2 +- src/calibre/gui2/actions/convert.py | 2 +- src/calibre/gui2/dialogs/catalog.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py index 9903801c6e..fadb140be6 100644 --- a/src/calibre/gui2/actions/catalog.py +++ b/src/calibre/gui2/actions/catalog.py @@ -17,7 +17,7 @@ from calibre.gui2.actions import InterfaceAction class GenerateCatalogAction(InterfaceAction): name = 'Generate Catalog' - action_spec = (_('Create catalog of books in your calibre library'), None, None, None) + action_spec = (_('Create a catalog of the books in your calibre library'), None, None, None) dont_add_to = frozenset(['toolbar-device', 'context-menu-device']) def generate_catalog(self): diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py index 821ebcd37f..caf65932d8 100644 --- a/src/calibre/gui2/actions/convert.py +++ b/src/calibre/gui2/actions/convert.py @@ -31,7 +31,7 @@ class ConvertAction(InterfaceAction): partial(self.convert_ebook, False, bulk=True)) cm.addSeparator() ac = cm.addAction( - _('Create catalog of books in your calibre library')) + _('Create a catalog of the books in your calibre library')) ac.triggered.connect(self.gui.iactions['Generate Catalog'].generate_catalog) self.qaction.setMenu(cm) self.qaction.triggered.connect(self.convert_ebook) diff --git a/src/calibre/gui2/dialogs/catalog.py b/src/calibre/gui2/dialogs/catalog.py index 7bb288ed20..7360d2ea5f 100644 --- a/src/calibre/gui2/dialogs/catalog.py +++ b/src/calibre/gui2/dialogs/catalog.py @@ -125,6 +125,10 @@ class Catalog(QDialog, Ui_Dialog): self.apply) self.show_plugin_tab(None) + geom = dynamic.get('catalog_window_geom', None) + if geom is not None: + self.restoreGeometry(bytes(geom)) + def show_plugin_tab(self, idx): cf = unicode(self.format.currentText()).lower() while self.tabs.count() > 1: @@ -157,6 +161,7 @@ class Catalog(QDialog, Ui_Dialog): dynamic.set('catalog_last_used_title', self.catalog_title) self.catalog_sync = bool(self.sync.isChecked()) dynamic.set('catalog_sync_to_device', self.catalog_sync) + dynamic.set('catalog_window_geom', bytearray(self.saveGeometry())) def apply(self): # Store current values without building catalog @@ -167,3 +172,8 @@ class Catalog(QDialog, Ui_Dialog): def accept(self): self.save_catalog_settings() return QDialog.accept(self) + + def reject(self): + dynamic.set('catalog_window_geom', bytearray(self.saveGeometry())) + QDialog.reject(self) + From 336e35c8ae2ab7ace44b7d281217ea488dcff565 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 09:47:42 -0700 Subject: [PATCH 10/32] Leduc by Brian Hahn. Fixes #8599 (New Recipe for Calibre - Weekly Newspaper) --- resources/recipes/leduc.recipe | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 resources/recipes/leduc.recipe diff --git a/resources/recipes/leduc.recipe b/resources/recipes/leduc.recipe new file mode 100644 index 0000000000..79ab693115 --- /dev/null +++ b/resources/recipes/leduc.recipe @@ -0,0 +1,40 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1292550626(BasicNewsRecipe): + title = 'Leduc - Wetaskiwin Pipestone Flyer' + __author__ = 'Brian Hahn' + description = 'News from Alberta, Canada' + oldest_article = 56 + max_articles_per_feed = 100 + no_stylesheets = True + #delay = 1 + use_embedded_content = False + publisher = 'Pipestone Publishing' + category = 'News, Alberta, Canada' + language = 'en_CA' + encoding = 'iso-8859-1' + cover_url = 'http://www.pipestoneflyer.ca/images/calibre-cover.jpg' + remove_tags_before = dict(id='ContentPanel') + remove_tags_after = dict(id='ContentPanel') + remove_tags = [dict(name='div', attrs={'id':'StoryNav'}),dict(name='div', attrs={'id':'BottomAds'}),dict(name='div', attrs={'id':'MoreStoryLinks'})] + extra_css = 'img { margin:5px }' + feeds = [ +('Feature', 'http://www.pipestoneflyer.ca/Feature.rss'), +('Editors Desk', 'http://www.pipestoneflyer.ca/Editor%27s%20Desk.rss'), +('Letters', 'http://www.pipestoneflyer.ca/Letters.rss'), +('A Loco Viewpoint', 'http://www.pipestoneflyer.ca/A%20Loco%20Viewpoint.rss'), +('Lifes Doorway', 'http://www.pipestoneflyer.ca/Life%27s%20Doorway.rss'), +('From the Otherside', 'http://www.pipestoneflyer.ca/From%20the%20Otherside.rss'), +('Opinion', 'http://www.pipestoneflyer.ca/Opinion.rss'), +('Community', 'http://www.pipestoneflyer.ca/Community.rss'), +('Sports', 'http://www.pipestoneflyer.ca/Sports.rss'), +('Chambers', 'http://www.pipestoneflyer.ca/Chambers.rss'), +('Government', 'http://www.pipestoneflyer.ca/Government.rss'), +('Environment', 'http://www.pipestoneflyer.ca/Environment.rss'), +('Health', 'http://www.pipestoneflyer.ca/Health.rss'), +('Funnies', 'http://www.pipestoneflyer.ca/Funnies.rss'), +('Faith', 'http://www.pipestoneflyer.ca/Faith.rss'), +('News and Views', 'http://www.pipestoneflyer.ca/News%20and%20Views.rss'), +('Obituaries', 'http://www.pipestoneflyer.ca/Obituaries.rss'), +('Police Blotter', 'http://www.pipestoneflyer.ca/Police%20Blotter.rss'), +] From bb19d8525b84daabb9f0828f247eb03eb8d96e13 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 10:06:16 -0700 Subject: [PATCH 11/32] Fix #8575 (Catalog dialog box sizing) --- src/calibre/gui2/dialogs/catalog.py | 21 ++-- src/calibre/gui2/dialogs/catalog.ui | 179 ++++++++++++++++------------ 2 files changed, 109 insertions(+), 91 deletions(-) diff --git a/src/calibre/gui2/dialogs/catalog.py b/src/calibre/gui2/dialogs/catalog.py index 7360d2ea5f..ebca7235eb 100644 --- a/src/calibre/gui2/dialogs/catalog.py +++ b/src/calibre/gui2/dialogs/catalog.py @@ -8,15 +8,12 @@ __docformat__ = 'restructuredtext en' import os, sys -from PyQt4 import QtGui -from PyQt4.Qt import QDialog, SIGNAL - from calibre.customize.ui import config from calibre.gui2.dialogs.catalog_ui import Ui_Dialog -from calibre.gui2 import dynamic +from calibre.gui2 import dynamic, ResizableDialog from calibre.customize.ui import catalog_plugins -class Catalog(QDialog, Ui_Dialog): +class Catalog(ResizableDialog, Ui_Dialog): ''' Catalog Dialog builder''' def __init__(self, parent, dbspec, ids, db): @@ -24,10 +21,8 @@ class Catalog(QDialog, Ui_Dialog): from calibre import prints as info from PyQt4.uic import compileUi - QDialog.__init__(self, parent) + ResizableDialog.__init__(self, parent) - # Run the dialog setup generated from catalog.ui - self.setupUi(self) self.dbspec, self.ids = dbspec, ids # Display the number of books we've been passed @@ -120,9 +115,7 @@ class Catalog(QDialog, Ui_Dialog): self.sync.setChecked(dynamic.get('catalog_sync_to_device', True)) self.format.currentIndexChanged.connect(self.show_plugin_tab) - self.connect(self.buttonBox.button(QtGui.QDialogButtonBox.Apply), - SIGNAL("clicked()"), - self.apply) + self.buttonBox.button(self.buttonBox.Apply).clicked.connect(self.apply) self.show_plugin_tab(None) geom = dynamic.get('catalog_window_geom', None) @@ -163,7 +156,7 @@ class Catalog(QDialog, Ui_Dialog): dynamic.set('catalog_sync_to_device', self.catalog_sync) dynamic.set('catalog_window_geom', bytearray(self.saveGeometry())) - def apply(self): + def apply(self, *args): # Store current values without building catalog self.save_catalog_settings() if self.tabs.count() > 1: @@ -171,9 +164,9 @@ class Catalog(QDialog, Ui_Dialog): def accept(self): self.save_catalog_settings() - return QDialog.accept(self) + return ResizableDialog.accept(self) def reject(self): dynamic.set('catalog_window_geom', bytearray(self.saveGeometry())) - QDialog.reject(self) + ResizableDialog.reject(self) diff --git a/src/calibre/gui2/dialogs/catalog.ui b/src/calibre/gui2/dialogs/catalog.ui index 62ac7cb5af..7f3951b87e 100644 --- a/src/calibre/gui2/dialogs/catalog.ui +++ b/src/calibre/gui2/dialogs/catalog.ui @@ -14,7 +14,7 @@ Generate catalog - + :/images/library.png:/images/library.png @@ -31,81 +31,6 @@ - - - - - 0 - 0 - - - - - 650 - 575 - - - - 0 - - - - Catalog options - - - - - - Catalog &format: - - - format - - - - - - - - - - Catalog &title (existing catalog with the same title will be replaced): - - - true - - - title - - - - - - - - - - &Send catalog to device automatically - - - - - - - Qt::Vertical - - - - 20 - 299 - - - - - - - - @@ -116,10 +41,110 @@ + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 666 + 599 + + + + + 0 + + + + + + 0 + 0 + + + + + 650 + 575 + + + + 0 + + + + Catalog options + + + + + + Catalog &format: + + + format + + + + + + + + + + Catalog &title (existing catalog with the same title will be replaced): + + + true + + + title + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + &Send catalog to device automatically + + + + + + + + + + + - + From b88fcdb8af62a5d1a80a842ba1d5f58a9916076c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 10:39:42 -0700 Subject: [PATCH 12/32] Fix #8539 (Author sort default should not put Inc. at the front for companies) --- src/calibre/ebooks/metadata/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 02401b25e6..847d69597c 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -36,9 +36,10 @@ def author_to_author_sort(author): return author author = _bracket_pat.sub('', author).strip() tokens = author.split() - tokens = tokens[-1:] + tokens[:-1] - if len(tokens) > 1 and method != 'nocomma': - tokens[0] += ',' + if tokens[-1] not in ('Inc.', 'Inc'): + tokens = tokens[-1:] + tokens[:-1] + if len(tokens) > 1 and method != 'nocomma': + tokens[0] += ',' return ' '.join(tokens) def authors_to_sort_string(authors): From b7a0c06bff0d0e3d525e580d21011492f2c4bfc2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 10:43:24 -0700 Subject: [PATCH 13/32] ... --- src/calibre/ebooks/metadata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 847d69597c..fcd4491fd3 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -36,7 +36,7 @@ def author_to_author_sort(author): return author author = _bracket_pat.sub('', author).strip() tokens = author.split() - if tokens[-1] not in ('Inc.', 'Inc'): + if tokens and tokens[-1] not in ('Inc.', 'Inc'): tokens = tokens[-1:] + tokens[:-1] if len(tokens) > 1 and method != 'nocomma': tokens[0] += ',' From d251703ee47d18dedc1b1119e631fa06ccb14e1a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 11:14:42 -0700 Subject: [PATCH 14/32] Fix #8577 (Adding empty book - cover browser doesn't update) --- src/calibre/gui2/actions/add.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 7c454d0a94..4236a63340 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -100,6 +100,9 @@ class AddAction(InterfaceAction): mi = MetaInformation(_('Unknown'), dlg.selected_authors) self.gui.library_view.model().db.import_book(mi, []) self.gui.library_view.model().books_added(num) + if hasattr(self.gui, 'db_images'): + self.gui.db_images.reset() + self.gui.tags_view.recount() def add_isbns(self, books, add_tags=[]): from calibre.ebooks.metadata import MetaInformation From 2c993c83abbb40f960cfa9bcbf006a21ce77c6ae Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 12:02:29 -0700 Subject: [PATCH 15/32] Create 'generic' output profiles and generic devices in the welcome wizard --- src/calibre/customize/profiles.py | 18 ++++++++- src/calibre/gui2/wizard/__init__.py | 57 +++++++++++++++-------------- src/calibre/gui2/wizard/device.ui | 2 +- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 763460d2ef..44628c22db 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -495,6 +495,22 @@ class SonyReader900Output(SonyReaderOutput): screen_size = (600, 999) comic_screen_size = screen_size +class GenericEink(SonyReaderOutput): + + name = 'Generic e-ink' + short_name = 'generic_eink' + description = _('Suitable for use with any e-ink device') + epub_periodical_format = None + +class GenericEinkLarge(GenericEink): + + name = 'Generic e-ink large' + short_name = 'generic_eink_large' + description = _('Suitable for use with any large screen e-ink device') + + screen_size = (600, 999) + comic_screen_size = screen_size + class JetBook5Output(OutputProfile): name = 'JetBook 5-inch' @@ -719,6 +735,6 @@ output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output, iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy, SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput, IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput, - BambookOutput, NookColorOutput] + BambookOutput, NookColorOutput, GenericEink, GenericEinkLarge] output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower())) diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py index 8144dcabf3..5f9f1828fa 100644 --- a/src/calibre/gui2/wizard/__init__.py +++ b/src/calibre/gui2/wizard/__init__.py @@ -33,10 +33,10 @@ from calibre.gui2.dialogs.progress import ProgressDialog class Device(object): - output_profile = 'default' + output_profile = 'generic_eink' output_format = 'EPUB' - name = 'Default' - manufacturer = 'Default' + name = 'Generic e-ink device' + manufacturer = 'Generic' id = 'default' supports_color = False @@ -63,6 +63,18 @@ class Device(object): recs['dont_grayscale'] = True save_defaults('comic_input', recs) +class Smartphone(Device): + + id = 'smartphone' + name = 'Smartphone' + supports_color = True + +class Tablet(Device): + + id = 'tablet' + name = 'iPad like tablet' + output_profile = 'tablet' + supports_color = True class Kindle(Device): @@ -206,12 +218,21 @@ class iPhone(Device): class Android(Device): - name = 'Adroid phone + WordPlayer/Aldiko' + name = 'Android phone' output_format = 'EPUB' manufacturer = 'Android' id = 'android' supports_color = True +class AndroidTablet(Device): + + name = 'Android tablet' + output_format = 'EPUB' + manufacturer = 'Android' + id = 'android_tablet' + supports_color = True + output_profile = 'tablet' + class HanlinV3(Device): name = 'Hanlin V3' @@ -268,9 +289,9 @@ def get_manufacturers(): mans = set([]) for x in get_devices(): mans.add(x.manufacturer) - if 'Default' in mans: - mans.remove('Default') - return ['Default'] + sorted(mans) + if Device.manufacturer in mans: + mans.remove(Device.manufacturer) + return [Device.manufacturer] + sorted(mans) def get_devices_of(manufacturer): ans = [d for d in get_devices() if d.manufacturer == manufacturer] @@ -402,22 +423,6 @@ class StanzaPage(QWizardPage, StanzaUI): except: continue -class WordPlayerPage(StanzaPage): - - ID = 6 - - def __init__(self): - StanzaPage.__init__(self) - self.label.setText('

'+_('If you use the WordPlayer e-book app on ' - 'your Android phone, you can access your calibre book collection ' - 'directly on the device. To do this you have to turn on the ' - 'content server.')) - self.instructions.setText('

'+_('Remember to leave calibre running ' - 'as the server only runs as long as calibre is running.')+'

' - + _('You have to add the URL http://myhostname:8080 as your ' - 'calibre library in WordPlayer. Here myhostname should be the fully ' - 'qualified hostname or the IP address of the computer calibre is running on.')) - class DevicePage(QWizardPage, DeviceUI): @@ -430,6 +435,8 @@ class DevicePage(QWizardPage, DeviceUI): self.registerField("device", self.device_view) def initializePage(self): + self.label.setText(_('Choose you e-book device. If your device is' + ' not in the list, choose a "%s" device.')%Device.manufacturer) self.man_model = ManufacturerModel() self.manufacturer_view.setModel(self.man_model) previous = dynamic.get('welcome_wizard_device', False) @@ -477,8 +484,6 @@ class DevicePage(QWizardPage, DeviceUI): return KindlePage.ID if dev is iPhone: return StanzaPage.ID - if dev is Android: - return WordPlayerPage.ID return FinishPage.ID class MoveMonitor(QObject): @@ -753,13 +758,11 @@ class Wizard(QWizard): self.set_finish_text() self.kindle_page = KindlePage() self.stanza_page = StanzaPage() - self.word_player_page = WordPlayerPage() self.setPage(self.library_page.ID, self.library_page) self.setPage(self.device_page.ID, self.device_page) self.setPage(self.finish_page.ID, self.finish_page) self.setPage(self.kindle_page.ID, self.kindle_page) self.setPage(self.stanza_page.ID, self.stanza_page) - self.setPage(self.word_player_page.ID, self.word_player_page) self.device_extra_page = None nh, nw = min_available_height()-75, available_width()-30 diff --git a/src/calibre/gui2/wizard/device.ui b/src/calibre/gui2/wizard/device.ui index 229f83be9e..ea120fb79b 100644 --- a/src/calibre/gui2/wizard/device.ui +++ b/src/calibre/gui2/wizard/device.ui @@ -27,7 +27,7 @@ - Choose your book reader. This will set the conversion options to produce books optimized for your device. + true From 6dc5304bed4f26c41f8be0e60be5fc8fc9db5cb9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 16:17:10 -0700 Subject: [PATCH 16/32] ... --- src/calibre/gui2/dialogs/metadata_single.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 00bc98cb17..ca2ccfa6d5 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -208,6 +208,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): from calibre.gui2 import config title = unicode(self.title.text()).strip() author = unicode(self.authors.text()).strip() + if author.endswith('&'): + author = author[:-1] if not title or not author: return error_dialog(self, _('Specify title and author'), _('You must specify a title and author before generating ' From 34931d4f734a0aed693ff689bce97f1a0af73030 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 16:30:06 -0700 Subject: [PATCH 17/32] BiBTeX Catalog: Add option to include file paths in the catalog. Fixes #8589 (Support for SciPlore MindMapping - Enhance Bib Catalogue) --- src/calibre/gui2/catalog/catalog_bibtex.py | 8 ++- src/calibre/gui2/catalog/catalog_bibtex.ui | 9 ++- src/calibre/library/catalog.py | 74 ++++++++++++++-------- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_bibtex.py b/src/calibre/gui2/catalog/catalog_bibtex.py index 7b7739bb46..ebfcc6e546 100644 --- a/src/calibre/gui2/catalog/catalog_bibtex.py +++ b/src/calibre/gui2/catalog/catalog_bibtex.py @@ -19,7 +19,9 @@ class PluginWidget(QWidget, Ui_Form): ('bib_entry', 0), #mixed ('bibfile_enc', 0), #utf-8 ('bibfile_enctag', 0), #strict - ('impcit', True) ] + ('impcit', True), + ('addfiles', False), + ] sync_enabled = False formats = set(['bib']) @@ -49,7 +51,7 @@ class PluginWidget(QWidget, Ui_Form): opt_value = gprefs.get(self.name + '_' + opt[0], opt[1]) if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']: getattr(self, opt[0]).setCurrentIndex(opt_value) - elif opt[0] == 'impcit' : + elif opt[0] in ['impcit', 'addfiles'] : getattr(self, opt[0]).setChecked(opt_value) else: getattr(self, opt[0]).setText(opt_value) @@ -76,7 +78,7 @@ class PluginWidget(QWidget, Ui_Form): for opt in self.OPTION_FIELDS: if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']: opt_value = getattr(self,opt[0]).currentIndex() - elif opt[0] == 'impcit' : + elif opt[0] in ['impcit', 'addfiles'] : opt_value = getattr(self, opt[0]).isChecked() else : opt_value = unicode(getattr(self, opt[0]).text()) diff --git a/src/calibre/gui2/catalog/catalog_bibtex.ui b/src/calibre/gui2/catalog/catalog_bibtex.ui index 7f4920655d..8712d40148 100644 --- a/src/calibre/gui2/catalog/catalog_bibtex.ui +++ b/src/calibre/gui2/catalog/catalog_bibtex.ui @@ -47,7 +47,7 @@ - + @@ -141,6 +141,13 @@ + + + Add files path with formats? + + + + Expression to form the BibTeX citation tag: diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index f0e4778de4..e20eebc517 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -24,10 +24,9 @@ from calibre.utils.logging import default_log as log from calibre.utils.zipfile import ZipFile, ZipInfo from calibre.utils.magick.draw import thumbnail -FIELDS = ['all', 'author_sort', 'authors', 'comments', - 'cover', 'formats', 'id', 'isbn', 'ondevice', 'pubdate', 'publisher', 'rating', - 'series_index', 'series', 'size', 'tags', 'timestamp', 'title', - 'uuid'] +FIELDS = ['all', 'title', 'author_sort', 'authors', 'comments', + 'cover', 'formats','id', 'isbn', 'ondevice', 'pubdate', 'publisher', + 'rating', 'series_index', 'series', 'size', 'tags', 'timestamp', 'uuid'] #Allowed fields for template TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate', @@ -252,6 +251,15 @@ class BIBTEX(CatalogPlugin): # {{{ "Default: '%default'\n" "Applies to: BIBTEX output format")), + Option('--add-files-path', + default = 'True', + dest = 'addfiles', + action = None, + help = _('Create a file entry if formats is selected for BibTeX entries.\n' + 'Boolean value: True, False\n' + "Default: '%default'\n" + "Applies to: BIBTEX output format")), + Option('--citation-template', default = '{authors}{id}', dest = 'bib_cit', @@ -298,7 +306,7 @@ class BIBTEX(CatalogPlugin): # {{{ from calibre.utils.bibtex import BibTeX def create_bibtex_entry(entry, fields, mode, template_citation, - bibtexdict, citation_bibtex = True): + bibtexdict, citation_bibtex=True, calibre_files=True): #Bibtex doesn't like UTF-8 but keep unicode until writing #Define starting chain or if book valid strict and not book return a Fail string @@ -360,8 +368,13 @@ class BIBTEX(CatalogPlugin): # {{{ bibtex_entry.append(u'isbn = "%s"' % re.sub(u'[\D]', u'', item)) elif field == 'formats' : - item = u', '.join([format.rpartition('.')[2].lower() for format in item]) - bibtex_entry.append(u'formats = "%s"' % item) + #Add file path if format is selected + formats = [format.rpartition('.')[2].lower() for format in item] + bibtex_entry.append(u'formats = "%s"' % u', '.join(formats)) + if calibre_files: + files = [u':%s:%s' % (format, format.rpartition('.')[2].upper())\ + for format in item] + bibtex_entry.append(u'files = "%s"' % u', '.join(files)) elif field == 'series_index' : bibtex_entry.append(u'volume = "%s"' % int(item)) @@ -510,32 +523,41 @@ class BIBTEX(CatalogPlugin): # {{{ citation_bibtex= True else : citation_bibtex= opts.impcit + + #Check add file entry and go to default in case of bad CLI + if isinstance(opts.addfiles, (StringType, UnicodeType)) : + if opts.addfiles == 'False' : + addfiles_bibtex = False + elif opts.addfiles == 'True' : + addfiles_bibtex = True + else : + log(" WARNING: incorrect --add-files-path, revert to default") + addfiles_bibtex= True + else : + addfiles_bibtex = opts.addfiles #Preprocess for error and light correction template_citation = preprocess_template(opts.bib_cit) #Open output and write entries - outfile = codecs.open(path_to_output, 'w', bibfile_enc, bibfile_enctag) + with codecs.open(path_to_output, 'w', bibfile_enc, bibfile_enctag)\ + as outfile: + #File header + nb_entries = len(data) + #check in book strict if all is ok else throw a warning into log + if bib_entry == 'book' : + nb_books = len(filter(check_entry_book_valid, data)) + if nb_books < nb_entries : + log(" WARNING: only %d entries in %d are book compatible" % (nb_books, nb_entries)) + nb_entries = nb_books - #File header - nb_entries = len(data) + outfile.write(u'%%%Calibre catalog\n%%%{0} entries in catalog\n\n'.format(nb_entries)) + outfile.write(u'@preamble{"This catalog of %d entries was generated by calibre on %s"}\n\n' + % (nb_entries, nowf().strftime("%A, %d. %B %Y %H:%M").decode(preferred_encoding))) - #check in book strict if all is ok else throw a warning into log - if bib_entry == 'book' : - nb_books = len(filter(check_entry_book_valid, data)) - if nb_books < nb_entries : - log(" WARNING: only %d entries in %d are book compatible" % (nb_books, nb_entries)) - nb_entries = nb_books - - outfile.write(u'%%%Calibre catalog\n%%%{0} entries in catalog\n\n'.format(nb_entries)) - outfile.write(u'@preamble{"This catalog of %d entries was generated by calibre on %s"}\n\n' - % (nb_entries, nowf().strftime("%A, %d. %B %Y %H:%M").decode(preferred_encoding))) - - for entry in data: - outfile.write(create_bibtex_entry(entry, fields, bib_entry, template_citation, - bibtexc, citation_bibtex)) - - outfile.close() + for entry in data: + outfile.write(create_bibtex_entry(entry, fields, bib_entry, template_citation, + bibtexc, citation_bibtex, addfiles_bibtex)) # }}} class EPUB_MOBI(CatalogPlugin): From 6a5bf738f23121fe16b3d83eb553a50364e6def5 Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 27 Jan 2011 18:30:36 -0500 Subject: [PATCH 18/32] TXT Input: Retain whitespace at the beginning of lines. --- src/calibre/ebooks/txt/processor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/txt/processor.py b/src/calibre/ebooks/txt/processor.py index 926e9a8dd6..206a18b3e4 100644 --- a/src/calibre/ebooks/txt/processor.py +++ b/src/calibre/ebooks/txt/processor.py @@ -20,9 +20,13 @@ HTML_TEMPLATE = u' Date: Thu, 27 Jan 2011 18:53:04 -0700 Subject: [PATCH 25/32] ... --- src/calibre/gui2/dialogs/scheduler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index 8aa624cacc..b6a3bed3eb 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -259,14 +259,14 @@ class Scheduler(QObject): if self.oldest > 0: delta = timedelta(days=self.oldest) try: - ids = self.recipe_model.db.tags_older_than(_('News'), delta) + ids = list(self.recipe_model.db.tags_older_than(_('News'), + delta)) except: # Should never happen ids = [] import traceback traceback.print_exc() if ids: - ids = list(ids) if ids: self.delete_old_news.emit(ids) QTimer.singleShot(60 * 60 * 1000, self.oldest_check) From 5241504d2a2f86afc93c3b06351b8055fc50622e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 19:04:59 -0700 Subject: [PATCH 26/32] Capes & Babes by skyhawker --- resources/recipes/capes_n_babes.recipe | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 resources/recipes/capes_n_babes.recipe diff --git a/resources/recipes/capes_n_babes.recipe b/resources/recipes/capes_n_babes.recipe new file mode 100644 index 0000000000..626c0817ef --- /dev/null +++ b/resources/recipes/capes_n_babes.recipe @@ -0,0 +1,10 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class CapesnBabesRecipe(BasicNewsRecipe): + title = u'Capes & Babes' + language = 'en' + __author__ = 'skyhawker' + oldest_article = 31 + max_articles_per_feed = 100 + use_embedded_content = True + feeds = [(u'Capes & Babes', u'feed://www.capesnbabes.com/feed/')] From 39beee5a91095d291a4499d7b282c967343e019d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 19:06:23 -0700 Subject: [PATCH 27/32] ... --- resources/recipes/capes_n_babes.recipe | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/recipes/capes_n_babes.recipe b/resources/recipes/capes_n_babes.recipe index 626c0817ef..f8f0946316 100644 --- a/resources/recipes/capes_n_babes.recipe +++ b/resources/recipes/capes_n_babes.recipe @@ -1,8 +1,9 @@ from calibre.web.feeds.news import BasicNewsRecipe class CapesnBabesRecipe(BasicNewsRecipe): - title = u'Capes & Babes' + title = u'Capes n Babes' language = 'en' + description = 'The Capes n Babes comic Blog' __author__ = 'skyhawker' oldest_article = 31 max_articles_per_feed = 100 From 1d6712cb53acf9c806e03531257114b98c172be0 Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 27 Jan 2011 21:42:25 -0500 Subject: [PATCH 28/32] Fix bug #6194: PML Input, support multi-level toc. --- src/calibre/ebooks/pml/pmlconverter.py | 76 ++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/src/calibre/ebooks/pml/pmlconverter.py b/src/calibre/ebooks/pml/pmlconverter.py index d2eb2c3736..20d8c7186b 100644 --- a/src/calibre/ebooks/pml/pmlconverter.py +++ b/src/calibre/ebooks/pml/pmlconverter.py @@ -143,7 +143,9 @@ class PML_HTMLizer(object): def __init__(self): self.state = {} - self.toc = TOC() + # toc consists of a tuple + # (level, (href, id, text)) + self.toc = [] self.file_name = '' def prepare_pml(self, pml): @@ -494,7 +496,7 @@ class PML_HTMLizer(object): output = [] self.state = {} - self.toc = TOC() + self.toc = [] self.file_name = file_name indent_state = {'t': False, 'T': False} @@ -542,6 +544,7 @@ class PML_HTMLizer(object): # inside of ="" so we don't have do special processing # for C. t = '' + level = 0 if c in 'XC': level = line.read(1) id = 'pml_toc-%s' % len(self.toc) @@ -553,7 +556,7 @@ class PML_HTMLizer(object): if not value or value == '': text = t else: - self.toc.add_item(os.path.basename(self.file_name), id, value) + self.toc.append((level, (os.path.basename(self.file_name), id, value))) text = '%s' % (t, id) elif c == 'm': empty = False @@ -624,7 +627,72 @@ class PML_HTMLizer(object): return output def get_toc(self): - return self.toc + ''' + Toc can have up to 5 levels, 0 - 4 inclusive. + + This function will add items to their appropriate + depth in the TOC tree. If the specified depth is + invalid (item would not have a valid parent) add + it to the next valid level above the specified + level. + ''' + # Base toc object all items will be added to. + n_toc = TOC() + # Used to track nodes in the toc so we can add + # sub items to the appropriate place in tree. + t_l0 = None + t_l1 = None + t_l2 = None + t_l3 = None + + for level, (href, id, text) in self.toc: + if level == u'0': + t_l0 = n_toc.add_item(href, id, text) + t_l1 = None + t_l2 = None + t_l3 = None + elif level == u'1': + if t_l0 == None: + t_l0 = n_toc + t_l1 = t_l0.add_item(href, id, text) + t_l2 = None + t_l3 = None + elif level == u'2': + if t_l1 == None: + if t_l0 == None: + t_l1 = n_toc + else: + t_l1 = t_l0 + t_l2 = t_l1.add_item(href, id, text) + t_l3 = None + elif level == u'3': + if t_l2 == None: + if t_l1 == None: + if t_l0 == None: + t_l2 = n_toc + else: + t_l2 = t_l0 + else: + t_l2 = t_l1 + t_l3 = t_l2.add_item(href, id, text) + # Level 4. + # Anything above 4 is invalid but we will count + # it as level 4. + else: + if t_l3 == None: + if t_l2 == None: + if t_l1 == None: + if t_l0 == None: + t_l3 = n_toc + else: + t_l3 = t_l0 + else: + t_l3 = t_l1 + else: + t_l3 = t_l2 + t_l3.add_item(href, id, text) + + return n_toc def pml_to_html(pml): From 103b6de0da2db1d0cfff09846a4c559a6d26fd38 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 19:56:13 -0700 Subject: [PATCH 29/32] Combine the database integrity check and library check into a single menu item. Also nicer implementation of the db integrity check. --- src/calibre/gui2/actions/choose_library.py | 127 +++++---------------- src/calibre/gui2/dialogs/check_library.py | 122 +++++++++++++++++++- src/calibre/gui2/ui.py | 5 +- src/calibre/library/database2.py | 82 ------------- src/calibre/library/prefs.py | 3 + 5 files changed, 153 insertions(+), 186 deletions(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index d45843995e..4fa327d274 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en' import os, shutil from functools import partial -from PyQt4.Qt import QMenu, Qt, QInputDialog, QThread, pyqtSignal, QProgressDialog +from PyQt4.Qt import QMenu, Qt, QInputDialog from calibre import isbytestring from calibre.constants import filesystem_encoding @@ -16,7 +16,7 @@ from calibre.utils.config import prefs from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \ question_dialog, info_dialog from calibre.gui2.actions import InterfaceAction -from calibre.gui2.dialogs.check_library import CheckLibraryDialog +from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck class LibraryUsageStats(object): # {{{ @@ -76,76 +76,6 @@ class LibraryUsageStats(object): # {{{ self.write_stats() # }}} -# Check Integrity {{{ - -class VacThread(QThread): - - check_done = pyqtSignal(object, object) - callback = pyqtSignal(object, object) - - def __init__(self, parent, db): - QThread.__init__(self, parent) - self.db = db - self._parent = parent - - def run(self): - err = bad = None - try: - bad = self.db.check_integrity(self.callbackf) - except: - import traceback - err = traceback.format_exc() - self.check_done.emit(bad, err) - - def callbackf(self, progress, msg): - self.callback.emit(progress, msg) - - -class CheckIntegrity(QProgressDialog): - - def __init__(self, db, parent=None): - QProgressDialog.__init__(self, parent) - self.db = db - self.setCancelButton(None) - self.setMinimum(0) - self.setMaximum(100) - self.setWindowTitle(_('Checking database integrity')) - self.setAutoReset(False) - self.setValue(0) - - self.vthread = VacThread(self, db) - self.vthread.check_done.connect(self.check_done, - type=Qt.QueuedConnection) - self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection) - self.vthread.start() - - def callback(self, progress, msg): - self.setLabelText(msg) - self.setValue(int(100*progress)) - - def check_done(self, bad, err): - if err: - error_dialog(self, _('Error'), - _('Failed to check database integrity'), - det_msg=err, show=True) - elif bad: - titles = [self.db.title(x, index_is_id=True) for x in bad] - det_msg = '\n'.join(titles) - warning_dialog(self, _('Some inconsistencies found'), - _('The following books had formats or covers listed in the ' - 'database that are not actually available. ' - 'The entries for the formats/covers have been removed. ' - 'You should check them manually. This can ' - 'happen if you manipulate the files in the ' - 'library folder directly.'), det_msg=det_msg, show=True) - else: - info_dialog(self, _('No errors found'), - _('The integrity check completed with no uncorrectable errors found.'), - show=True) - self.reset() - -# }}} - class ChooseLibraryAction(InterfaceAction): name = 'Choose Library' @@ -209,14 +139,6 @@ class ChooseLibraryAction(InterfaceAction): None, None), attr='action_check_library') ac.triggered.connect(self.check_library, type=Qt.QueuedConnection) self.maintenance_menu.addAction(ac) - ac = self.create_action(spec=(_('Check database integrity'), 'lt.png', - None, None), attr='action_check_database') - ac.triggered.connect(self.check_database, type=Qt.QueuedConnection) - self.maintenance_menu.addAction(ac) - ac = self.create_action(spec=(_('Recover database'), 'lt.png', - None, None), attr='action_restore_database') - ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection) - self.maintenance_menu.addAction(ac) self.choose_menu.addMenu(self.maintenance_menu) def pick_random(self, *args): @@ -346,28 +268,35 @@ class ChooseLibraryAction(InterfaceAction): 'rate of approximately 1 book every three seconds.'), show=True) def check_library(self): - db = self.gui.library_view.model().db - d = CheckLibraryDialog(self.gui.parent(), db) - d.exec_() - - def check_database(self, *args): + self.gui.library_view.save_state() m = self.gui.library_view.model() m.stop_metadata_backup() - try: - d = CheckIntegrity(m.db, self.gui) - d.exec_() - finally: - m.start_metadata_backup() + db = m.db + db.prefs.disable_setting = True - def restore_database(self): - info_dialog(self.gui, _('Recover database'), '

'+ - _( - 'This command rebuilds your calibre database from the information ' - 'stored by calibre in the OPF files.

' - 'This function is not currently available in the GUI. You can ' - 'recover your database using the \'calibredb restore_database\' ' - 'command line function.' - ), show=True) + d = DBCheck(self.gui, db) + d.start() + try: + d.conn.close() + except: + pass + d.break_cycles() + self.gui.library_moved(db.library_path, call_close=not + d.closed_orig_conn) + if d.rejected: + return + if d.error is None: + if not question_dialog(self.gui, _('Success'), + _('Found no errors in your calibre library database.' + ' Do you want calibre to check if the files in your ' + ' library match the information in the database?')): + return + else: + return error_dialog(self.gui, _('Failed'), + _('Database integrity check failed, click Show details' + ' for details.'), show=True, det_msg=d.error[1]) + d = CheckLibraryDialog(self.gui, m.db) + d.exec_() def switch_requested(self, location): if not self.change_library_allowed(): diff --git a/src/calibre/gui2/dialogs/check_library.py b/src/calibre/gui2/dialogs/check_library.py index c00ee99cc0..1c199afc03 100644 --- a/src/calibre/gui2/dialogs/check_library.py +++ b/src/calibre/gui2/dialogs/check_library.py @@ -3,16 +3,132 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __license__ = 'GPL v3' -import os +import os, shutil from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \ QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \ - QLineEdit, Qt + QLineEdit, Qt, QProgressBar, QSize, QTimer from calibre.gui2.dialogs.confirm_delete import confirm from calibre.library.check_library import CheckLibrary, CHECKS from calibre.library.database2 import delete_file, delete_tree -from calibre import prints +from calibre import prints, as_unicode +from calibre.ptempfile import PersistentTemporaryFile +from calibre.library.sqlite import DBThread, OperationalError + +class DBCheck(QDialog): + + def __init__(self, parent, db): + QDialog.__init__(self, parent) + self.l = QVBoxLayout() + self.setLayout(self.l) + self.l1 = QLabel(_('Checking database integrity')+'...') + self.setWindowTitle(_('Checking database integrity')) + self.l.addWidget(self.l1) + self.pb = QProgressBar(self) + self.l.addWidget(self.pb) + self.pb.setMaximum(0) + self.pb.setMinimum(0) + self.msg = QLabel('') + self.l.addWidget(self.msg) + self.msg.setWordWrap(True) + self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) + self.l.addWidget(self.bb) + self.bb.rejected.connect(self.reject) + self.resize(self.sizeHint() + QSize(100, 50)) + self.error = None + self.db = db + self.closed_orig_conn = False + + def start(self): + self.user_version = self.db.user_version + self.rejected = False + self.db.clean() + self.db.conn.close() + self.closed_orig_conn = True + t = DBThread(self.db.dbpath, False) + t.connect() + self.conn = t.conn + self.dump = self.conn.iterdump() + self.statements = [] + self.count = 0 + self.msg.setText(_('Dumping database to SQL')) + # Give the backup thread time to stop + QTimer.singleShot(2000, self.do_one_dump) + self.exec_() + + def do_one_dump(self): + if self.rejected: + return + try: + try: + self.statements.append(self.dump.next()) + self.count += 1 + except StopIteration: + self.start_load() + return + QTimer.singleShot(0, self.do_one_dump) + except Exception, e: + import traceback + self.error = (as_unicode(e), traceback.format_exc()) + self.reject() + + def start_load(self): + self.conn.close() + self.pb.setMaximum(self.count) + self.pb.setValue(0) + self.msg.setText(_('Loading database from SQL')) + self.db.conn.close() + self.ndbpath = PersistentTemporaryFile('.db') + self.ndbpath.close() + self.ndbpath = self.ndbpath.name + t = DBThread(self.ndbpath, False) + t.connect() + self.conn = t.conn + self.conn.execute('create temporary table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)') + self.conn.commit() + + QTimer.singleShot(0, self.do_one_load) + + def do_one_load(self): + if self.rejected: + return + if self.count > 0: + try: + try: + self.conn.execute(self.statements.pop(0)) + except OperationalError: + if self.count > 1: + # The last statement in the dump could be an extra + # commit, so ignore it. + raise + self.pb.setValue(self.pb.value() + 1) + self.count -= 1 + QTimer.singleShot(0, self.do_one_load) + except Exception, e: + import traceback + self.error = (as_unicode(e), traceback.format_exc()) + self.reject() + + else: + self.replace_db() + + def replace_db(self): + self.conn.commit() + self.conn.execute('pragma user_version=%d'%int(self.user_version)) + self.conn.commit() + self.conn.close() + shutil.copyfile(self.ndbpath, self.db.dbpath) + self.db = None + self.accept() + + def break_cycles(self): + self.statements = self.unpickler = self.db = self.conn = None + + def reject(self): + self.rejected = True + QDialog.reject(self) + class Item(QTreeWidgetItem): pass diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index c0658536bb..5fe630691c 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -408,7 +408,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ def booklists(self): return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db - def library_moved(self, newloc, copy_structure=False): + def library_moved(self, newloc, copy_structure=False, call_close=True): if newloc is None: return default_prefs = None try: @@ -441,7 +441,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.apply_named_search_restriction(db.prefs['gui_restriction']) if olddb is not None: try: - olddb.conn.close() + if call_close: + olddb.conn.close() except: import traceback traceback.print_exc() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index b6c377adff..9fac071492 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -2795,86 +2795,4 @@ books_series_link feeds for id, title, script in self.conn.get('SELECT id,title,script FROM feeds'): yield id, title, script - def reconnect(self): - 'Used to reconnect after calling self.conn.close()' - self.connect() - self.initialize_dynamic() - self.refresh() - def check_integrity(self, callback): - callback(0., _('Checking SQL integrity...')) - self.clean() - user_version = self.user_version - sql = '\n'.join(self.conn.dump()) - self.conn.close() - dest = self.dbpath+'.tmp' - if os.path.exists(dest): - os.remove(dest) - conn = None - try: - ndb = DBThread(dest, None) - ndb.connect() - conn = ndb.conn - conn.execute('create table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)') - conn.commit() - conn.executescript(sql) - conn.commit() - conn.execute('pragma user_version=%d'%user_version) - conn.commit() - conn.execute('drop table temp_sequence') - conn.commit() - conn.close() - except: - if conn is not None: - try: - conn.close() - except: - pass - if os.path.exists(dest): - os.remove(dest) - raise - else: - shutil.copyfile(dest, self.dbpath) - self.reconnect() - if os.path.exists(dest): - os.remove(dest) - callback(0.1, _('Checking for missing files.')) - bad = {} - us = self.data.universal_set() - total = float(len(us)) - for i, id in enumerate(us): - formats = self.data.get(id, self.FIELD_MAP['formats'], row_is_id=True) - if not formats: - formats = [] - else: - formats = [x.lower() for x in formats.split(',')] - actual_formats = self.formats(id, index_is_id=True) - if not actual_formats: - actual_formats = [] - else: - actual_formats = [x.lower() for x in actual_formats.split(',')] - - for fmt in formats: - if fmt in actual_formats: - continue - if id not in bad: - bad[id] = [] - bad[id].append(fmt) - has_cover = self.data.get(id, self.FIELD_MAP['cover'], - row_is_id=True) - if has_cover and self.cover(id, index_is_id=True, as_path=True) is None: - if id not in bad: - bad[id] = [] - bad[id].append('COVER') - callback(0.1+0.9*(1+i)/total, _('Checked id') + ' %d'%id) - - for id in bad: - for fmt in bad[id]: - if fmt != 'COVER': - self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, fmt.upper())) - else: - self.conn.execute('UPDATE books SET has_cover=0 WHERE id=?', (id,)) - self.conn.commit() - self.refresh_ids(list(bad.keys())) - - return bad diff --git a/src/calibre/library/prefs.py b/src/calibre/library/prefs.py index 2921e1c936..233c717897 100644 --- a/src/calibre/library/prefs.py +++ b/src/calibre/library/prefs.py @@ -17,6 +17,7 @@ class DBPrefs(dict): dict.__init__(self) self.db = db self.defaults = {} + self.disable_setting = False for key, val in self.db.conn.get('SELECT key,val FROM preferences'): try: val = self.raw_to_object(val) @@ -45,6 +46,8 @@ class DBPrefs(dict): self.db.conn.commit() def __setitem__(self, key, val): + if self.disable_setting: + return raw = self.to_raw(val) self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,)) self.db.conn.execute('INSERT INTO preferences (key,val) VALUES (?,?)', (key, From 6eb59e92680b0da73b4b6242176dae35a580d4e3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 19:56:59 -0700 Subject: [PATCH 30/32] ... --- src/calibre/library/database2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 9fac071492..3c6d4016f2 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -21,7 +21,7 @@ from calibre.library.field_metadata import FieldMetadata, TagsIcons from calibre.library.schema_upgrades import SchemaUpgrade from calibre.library.caches import ResultCache from calibre.library.custom_columns import CustomColumns -from calibre.library.sqlite import connect, IntegrityError, DBThread +from calibre.library.sqlite import connect, IntegrityError from calibre.library.prefs import DBPrefs from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.ebooks.metadata.book.base import Metadata From e37e14b01793545e8dd08737cec07b46462b65e5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 20:06:04 -0700 Subject: [PATCH 31/32] SPIN Magazine by Quistopher --- resources/recipes/spin_magazine.recipe | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 resources/recipes/spin_magazine.recipe diff --git a/resources/recipes/spin_magazine.recipe b/resources/recipes/spin_magazine.recipe new file mode 100644 index 0000000000..dbac216f81 --- /dev/null +++ b/resources/recipes/spin_magazine.recipe @@ -0,0 +1,15 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1296179411(BasicNewsRecipe): + title = u'SPIN Magzine' + __author__ = 'Quistopher' + language = 'en' + oldest_article = 7 + max_articles_per_feed = 100 + + feeds = [ + (u'Daily Noise Blog | SPIN.com', u'http://www.spin.com/blog/feed'), + (u'It Happened Last Night | SPIN.com', u'http://www.spin.com/it-happened-last-night/feed'), + (u'Album Reviews | SPIN.com', u'http://www.spin.com/album-reviews/feed') + + ] From 05cf89a1f03c42295bc4002232cb2ffdfb50b7ee Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jan 2011 22:36:42 -0700 Subject: [PATCH 32/32] ... --- src/calibre/gui2/widgets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 41b18aebba..afbe6b5d8c 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -75,13 +75,13 @@ class FilenamePattern(QWidget, Ui_Form): # has added. val_hist = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())] self.re.clear() - + if defaults: val = prefs.defaults['filename_pattern'] else: val = prefs['filename_pattern'] self.re.lineEdit().setText(val) - + val_hist += gprefs.get('filename_pattern_history', ['(?P.+)', '(?P<author>[^_-]+) -?\s*(?P<series>[^_0-9-]*)(?P<series_index>[0-9]*)\s*-\s*(?P<title>[^_].+) ?']) if val in val_hist: del val_hist[val_hist.index(val)] @@ -129,15 +129,15 @@ class FilenamePattern(QWidget, Ui_Form): def commit(self): pat = self.pattern().pattern prefs['filename_pattern'] = pat - + history = [] history_pats = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())] for p in history_pats[:14]: # Ensure we don't have duplicate items. if p and p not in history: history.append(p) - gprefs['filename_pattern_history'] = history - + gprefs['filename_pattern_history'] = history + return pat