From af9d22fa52897b64380fd172184f35365c722b3e Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 11 Mar 2011 11:27:33 -0500 Subject: [PATCH 01/41] Allow empty username and password for SMTP server --- src/calibre/gui2/wizard/send_email.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py index 5785f52276..9ac540e087 100644 --- a/src/calibre/gui2/wizard/send_email.py +++ b/src/calibre/gui2/wizard/send_email.py @@ -92,7 +92,8 @@ class SendEmail(QWidget, Ui_Form): pa = self.preferred_to_address() to_set = pa is not None if self.set_email_settings(to_set): - if question_dialog(self, _('OK to proceed?'), + opts = smtp_prefs().parse() + if not opts.relay_password or question_dialog(self, _('OK to proceed?'), _('This will display your email password on the screen' '. Is it OK to proceed?'), show_copy_button=False): TestEmail(pa, self).exec_() @@ -204,11 +205,19 @@ class SendEmail(QWidget, Ui_Form): username = unicode(self.relay_username.text()).strip() password = unicode(self.relay_password.text()).strip() host = unicode(self.relay_host.text()).strip() - if host and not (username and password): + if host and ((username and not password) or (not username and password)): error_dialog(self, _('Bad configuration'), - _('You must set the username and password for ' - 'the mail server.')).exec_() + _('You must either set the username and password for ' + 'the mail server or no username and no password at all.')).exec_() return False + elif host and not (username and password) and self.relay_ssl.isChecked(): + error_dialog(self, _('Bad configuration'), + _('Please enter username and password for SSL encryption. ')).exec_() + return False + elif host and not (username and password) and not question_dialog(self, + _('Are you sure?'), + _('No username and password set for mailserver. Continue anyways?')): + return False conf = smtp_prefs() conf.set('from_', from_) conf.set('relay_host', host if host else None) From 690e68cf9d28c87de0c70ec60f7bc5ed6eb11883 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 12 Mar 2011 09:29:57 +0000 Subject: [PATCH 02/41] Small documentation updates --- src/calibre/manual/sub_groups.rst | 10 ++++++++++ src/calibre/manual/template_lang.rst | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/calibre/manual/sub_groups.rst b/src/calibre/manual/sub_groups.rst index 83b8f0cbe9..edfc81d3d4 100644 --- a/src/calibre/manual/sub_groups.rst +++ b/src/calibre/manual/sub_groups.rst @@ -105,3 +105,13 @@ After creating the saved search, you can use it as a restriction. .. image:: images/sg_restrict2.jpg :align: center + Useful Template Functions + ------------------------- + + You might want to use the genre information in a template, such as with save to disk or send to device. The question might then be "How do I get the outermost genre name or names?" An |app| template function, subitems, is provided to make doing this easier. + + For example, assume you want to add the outermost genre level to the save-to-disk template to make genre folders, as in "History/The Gathering Storm - Churchill, Winston". To do this, you must extract the first level of the hierarchy and add it to the front along with a slash to indicate that it should make a folder. The template below accomplishes this:: + + {#genre:subitems(0,1)||/}{title} - {authors} + +See :ref:`The |app| template language ` for more information templates and the subitem function. \ No newline at end of file diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index b686d78981..c6e29e3915 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -129,7 +129,7 @@ The functions available are: * ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want. * ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later). * ``select(key)`` -- interpret the field as a comma-separated list of items, with the items being of the form "id:value". Find the pair with the id equal to key, and return the corresponding value. This function is particularly useful for extracting a value such as an isbn from the set of identifiers for a book. - * ``subitems(val, start_index, end_index)`` -- This function is used to break apart lists of tag-like hierarchical items such as genres. It interprets the value as a comma-separated list of tag-like items, where each item is a period-separated list. Returns a new list made by first finding all the period-separated tag-like items, then for each such item extracting the `start_index` th to the `end_index` th components, then combining the results back together. The first component in a period-separated list has an index of zero. If an index is negative, then it counts from the end of the list. As a special case, an end_index of zero is assumed to be the length of the list. Examples:: + * ``subitems(val, start_index, end_index)`` -- This function is used to break apart lists of tag-like hierarchical items such as genres. It interprets the value as a comma-separated list of tag-like items, where each item is a period-separated list. Returns a new list made by first finding all the period-separated tag-like items, then for each such item extracting the components from `start_index` to `end_index`, then combining the results back together. The first component in a period-separated list has an index of zero. If an index is negative, then it counts from the end of the list. As a special case, an end_index of zero is assumed to be the length of the list. Examples:: Assuming a #genre column containing "A.B.C": {#genre:subitems(0,1)} returns "A" @@ -139,7 +139,7 @@ The functions available are: {#genre:subitems(0,1)} returns "A, D" {#genre:subitems(0,2)} returns "A.B, D.E" - * ``sublist(val, start_index, end_index, separator)`` -- interpret the value as a list of items separated by `separator`, returning a new list made from the `start_index` th to the `end_index` th item. The first item is number zero. If an index is negative, then it counts from the end of the list. As a special case, an end_index of zero is assumed to be the length of the list. Examples assuming that the tags column (which is comma-separated) contains "A, B ,C":: + * ``sublist(val, start_index, end_index, separator)`` -- interpret the value as a list of items separated by `separator`, returning a new list made from the items from `start_index`to `end_index`. The first item is number zero. If an index is negative, then it counts from the end of the list. As a special case, an end_index of zero is assumed to be the length of the list. Examples assuming that the tags column (which is comma-separated) contains "A, B ,C":: {tags:sublist(0,1,\,)} returns "A" {tags:sublist(-1,0,\,)} returns "C" From af22656188f4e4699e08dade54a35e1cbfc43a56 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 12 Mar 2011 12:06:08 +0000 Subject: [PATCH 03/41] 1) add force_changes to set_metadata 2) add a 'default=' parameter to Metadata.get_extra, preventing exceptions --- src/calibre/ebooks/metadata/book/base.py | 7 +++-- src/calibre/library/database2.py | 36 +++++++++++++++--------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index feb6ff4bb9..366d12e5be 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -169,10 +169,13 @@ class Metadata(object): pass return default - def get_extra(self, field): + def get_extra(self, field, default=None): _data = object.__getattribute__(self, '_data') if field in _data['user_metadata'].iterkeys(): - return _data['user_metadata'][field]['#extra#'] + try: + return _data['user_metadata'][field]['#extra#'] + except: + return default raise AttributeError( 'Metadata object has no attribute named: '+ repr(field)) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index d03975baea..e8467aaa50 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1690,8 +1690,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.notify('metadata', [id]) return books_to_refresh - def set_metadata(self, id, mi, ignore_errors=False, - set_title=True, set_authors=True, commit=True): + def set_metadata(self, id, mi, ignore_errors=False, set_title=True, + set_authors=True, commit=True, force_changes=False): ''' Set metadata for the book `id` from the `Metadata` object `mi` ''' @@ -1707,6 +1707,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): traceback.print_exc() else: raise + # force_changes has no role to play in setting title or author path_changed = False if set_title and mi.title: self._set_title(id, mi.title) @@ -1721,16 +1722,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): path_changed = True if path_changed: self.set_path(id, index_is_id=True) - if mi.author_sort: + + if force_changes or mi.author_sort: doit(self.set_author_sort, id, mi.author_sort, notify=False, commit=False) - if mi.publisher: + if force_changes or mi.publisher: doit(self.set_publisher, id, mi.publisher, notify=False, commit=False) - if mi.rating: + if force_changes or mi.rating: doit(self.set_rating, id, mi.rating, notify=False, commit=False) - if mi.series: + if force_changes or mi.series: doit(self.set_series, id, mi.series, notify=False, commit=False) + if mi.cover_data[1] is not None: doit(self.set_cover, id, mi.cover_data[1], commit=False) elif mi.cover is not None: @@ -1739,13 +1742,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): raw = f.read() if raw: doit(self.set_cover, id, raw, commit=False) - if mi.tags: + elif force_changes: + doit(self.remove_cover, id, notify=False, commit=False) + + if force_changes or mi.tags: doit(self.set_tags, id, mi.tags, notify=False, commit=False) - if mi.comments: + if force_changes or mi.comments: doit(self.set_comment, id, mi.comments, notify=False, commit=False) - if mi.series_index: + if force_changes or mi.series_index: doit(self.set_series_index, id, mi.series_index, notify=False, commit=False) + + # force_changes would have no effect on the next two. if mi.pubdate: doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False) if getattr(mi, 'timestamp', None) is not None: @@ -1756,7 +1764,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if mi_idents: identifiers = self.get_identifiers(id, index_is_id=True) for key, val in mi_idents.iteritems(): - if val and val.strip(): # Don't delete an existing identifier + if force_changes or (val and val.strip()): identifiers[icu_lower(key)] = val self.set_identifiers(id, identifiers, notify=False, commit=False) @@ -1765,10 +1773,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for key in user_mi.iterkeys(): if key in self.field_metadata and \ user_mi[key]['datatype'] == self.field_metadata[key]['datatype']: - doit(self.set_custom, id, - val=mi.get(key), - extra=mi.get_extra(key), - label=user_mi[key]['label'], commit=False) + val = mi.get(key, None) + if force_changes or val: + doit(self.set_custom, id, val=val, extra=mi.get_extra(key), + label=user_mi[key]['label'], commit=False) if commit: self.conn.commit() self.notify('metadata', [id]) From 6dcdd98ef4fa639c7395157a778f2d445d20661f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Mar 2011 09:49:46 -0700 Subject: [PATCH 04/41] Allow icons to be loaded in the toolbar preferences for external plugins --- src/calibre/gui2/preferences/toolbar.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/gui2/preferences/toolbar.py b/src/calibre/gui2/preferences/toolbar.py index 26cdea19d3..a0d48f3910 100644 --- a/src/calibre/gui2/preferences/toolbar.py +++ b/src/calibre/gui2/preferences/toolbar.py @@ -55,6 +55,10 @@ class BaseModel(QAbstractListModel): text = _('Choose library') return QVariant(text) if role == Qt.DecorationRole: + if hasattr(self._data[row], 'qaction'): + icon = self._data[row].qaction.icon() + if not icon.isNull(): + return QVariant(icon) ic = action[1] if ic is None: ic = 'blank.png' From b9d90b43fb0b2c7a72fc4a6980ce644c00ffaac8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Mar 2011 10:32:20 -0700 Subject: [PATCH 05/41] ... --- resources/recipes/instapaper.recipe | 34 +++++------------------------ 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/resources/recipes/instapaper.recipe b/resources/recipes/instapaper.recipe index 73c32d08a7..0eb5cf0f09 100644 --- a/resources/recipes/instapaper.recipe +++ b/resources/recipes/instapaper.recipe @@ -1,23 +1,12 @@ -__license__ = 'GPL v3' -__copyright__ = '2009-2010, Darko Miletic ' -''' -www.instapaper.com -''' - -import urllib from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe -class Instapaper(BasicNewsRecipe): - title = 'Instapaper.com' +class AdvancedUserRecipe1299694372(BasicNewsRecipe): + title = u'Instapaper' __author__ = 'Darko Miletic' - description = '''Personalized news feeds. Go to instapaper.com to - setup up your news. Fill in your instapaper - username, and leave the password field - below blank.''' publisher = 'Instapaper.com' - category = 'news, custom' - oldest_article = 7 + category = 'info, custom, Instapaper' + oldest_article = 365 max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False @@ -25,16 +14,9 @@ class Instapaper(BasicNewsRecipe): INDEX = u'http://www.instapaper.com' LOGIN = INDEX + u'/user/login' - conversion_options = { - 'comment' : description - , 'tags' : category - , 'publisher' : publisher - } - feeds = [ - (u'Unread articles' , INDEX + u'/u' ) - ,(u'Starred articles', INDEX + u'/starred') - ] + + feeds = [(u'Instapaper Unread', u'http://www.instapaper.com/u'), (u'Instapaper Starred', u'http://www.instapaper.com/starred')] def get_browser(self): br = BasicNewsRecipe.get_browser() @@ -70,7 +52,3 @@ class Instapaper(BasicNewsRecipe): }) totalfeeds.append((feedtitle, articles)) return totalfeeds - - def print_version(self, url): - return self.INDEX + '/text?u=' + urllib.quote(url) - From 92ee6175a7dfac7b34bdf2490fedfc8b0456dcd3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 12 Mar 2011 19:02:19 +0000 Subject: [PATCH 06/41] Change set_metadata to use is_null --- src/calibre/ebooks/metadata/book/base.py | 2 +- src/calibre/library/database2.py | 51 +++++++++++++----------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 366d12e5be..7f868a298a 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -92,7 +92,7 @@ class Metadata(object): def is_null(self, field): null_val = NULL_VALUES.get(field, None) val = getattr(self, field, None) - return not val or val == null_val + return val is None or val == null_val def __getattribute__(self, field): _data = object.__getattribute__(self, '_data') diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index e8467aaa50..52deee1f93 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1691,7 +1691,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return books_to_refresh def set_metadata(self, id, mi, ignore_errors=False, set_title=True, - set_authors=True, commit=True, force_changes=False): + set_authors=True, commit=True, force_cover=False, + force_tags=False): ''' Set metadata for the book `id` from the `Metadata` object `mi` ''' @@ -1709,12 +1710,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): raise # force_changes has no role to play in setting title or author path_changed = False - if set_title and mi.title: + if set_title and not mi.is_null('title'): self._set_title(id, mi.title) path_changed = True - if set_authors: - if not mi.authors: - mi.authors = [_('Unknown')] + if set_authors and not mi.is_null('authors'): authors = [] for a in mi.authors: authors += string_to_authors(a) @@ -1723,15 +1722,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if path_changed: self.set_path(id, index_is_id=True) - if force_changes or mi.author_sort: + if not mi.is_null('author_sort'): doit(self.set_author_sort, id, mi.author_sort, notify=False, commit=False) - if force_changes or mi.publisher: + if not mi.is_null('publisher'): doit(self.set_publisher, id, mi.publisher, notify=False, commit=False) - if force_changes or mi.rating: + if not mi.is_null('rating'): doit(self.set_rating, id, mi.rating, notify=False, commit=False) - if force_changes or mi.series: + if not mi.is_null('series'): doit(self.set_series, id, mi.series, notify=False, commit=False) if mi.cover_data[1] is not None: @@ -1742,19 +1741,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): raw = f.read() if raw: doit(self.set_cover, id, raw, commit=False) - elif force_changes: + elif force_cover: doit(self.remove_cover, id, notify=False, commit=False) - if force_changes or mi.tags: + if force_tags or not mi.is_null('tags'): doit(self.set_tags, id, mi.tags, notify=False, commit=False) - if force_changes or mi.comments: + if not mi.is_null('comments'): doit(self.set_comment, id, mi.comments, notify=False, commit=False) - if force_changes or mi.series_index: + if not mi.is_null('series_index'): doit(self.set_series_index, id, mi.series_index, notify=False, commit=False) - - # force_changes would have no effect on the next two. - if mi.pubdate: + if not mi.is_null('pubdate'): doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False) if getattr(mi, 'timestamp', None) is not None: doit(self.set_timestamp, id, mi.timestamp, notify=False, @@ -1764,19 +1761,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if mi_idents: identifiers = self.get_identifiers(id, index_is_id=True) for key, val in mi_idents.iteritems(): - if force_changes or (val and val.strip()): + if val and val.strip(): identifiers[icu_lower(key)] = val self.set_identifiers(id, identifiers, notify=False, commit=False) - user_mi = mi.get_all_user_metadata(make_copy=False) for key in user_mi.iterkeys(): if key in self.field_metadata and \ user_mi[key]['datatype'] == self.field_metadata[key]['datatype']: - val = mi.get(key, None) - if force_changes or val: - doit(self.set_custom, id, val=val, extra=mi.get_extra(key), - label=user_mi[key]['label'], commit=False) + doit(self.set_custom, id, val=mi.get(key), commit=False, + extra=mi.get_extra(key), label=user_mi[key]['label']) if commit: self.conn.commit() self.notify('metadata', [id]) @@ -2366,6 +2360,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): @param tags: list of strings @param append: If True existing tags are not removed ''' + if tags is None: + tags = [] if not append: self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,)) self.conn.execute('''DELETE FROM tags WHERE (SELECT COUNT(id) @@ -2516,6 +2512,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.notify('metadata', [id]) def set_rating(self, id, rating, notify=True, commit=True): + if rating is None: + rating = 0 rating = int(rating) self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,)) rat = self.conn.get('SELECT id FROM ratings WHERE rating=?', (rating,), all=False) @@ -2530,7 +2528,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def set_comment(self, id, text, notify=True, commit=True): self.conn.execute('DELETE FROM comments WHERE book=?', (id,)) - self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text)) + if text: + self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text)) + else: + text = '' if commit: self.conn.commit() self.data.set(id, self.FIELD_MAP['comments'], text, row_is_id=True) @@ -2539,6 +2540,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.notify('metadata', [id]) def set_author_sort(self, id, sort, notify=True, commit=True): + if sort is None: + sort = '' self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (sort, id)) self.dirtied([id], commit=False) if commit: @@ -2610,6 +2613,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def set_identifiers(self, id_, identifiers, notify=True, commit=True): cleaned = {} + if identifiers is None: + identifiers = {} for typ, val in identifiers.iteritems(): typ, val = self._clean_identifier(typ, val) if val: From 4bfa19bbd7cb6e0a99dc124ec28a64888a776d3d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 13 Mar 2011 10:25:29 +0000 Subject: [PATCH 07/41] Another try at set_metadata w/force_changes --- src/calibre/ebooks/metadata/book/base.py | 4 +- src/calibre/library/database2.py | 65 +++++++++++++++++------- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 41b913455a..16d677b466 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -92,8 +92,6 @@ class Metadata(object): def is_null(self, field): null_val = NULL_VALUES.get(field, None) val = getattr(self, field, None) - if val is False or val in (0, 0.0): - return True return not val or val == null_val def __getattribute__(self, field): @@ -129,6 +127,8 @@ class Metadata(object): field, val = self._clean_identifier(field, val) _data['identifiers'].update({field: val}) elif field == 'identifiers': + if val is None: + val = copy.copy(NULL_VALUES.get('identifiers', None)) self.set_identifiers(val) elif field in STANDARD_METADATA_FIELDS: if val is None: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index e46f9b818d..ea30c08aff 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1691,10 +1691,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return books_to_refresh def set_metadata(self, id, mi, ignore_errors=False, set_title=True, - set_authors=True, commit=True, force_cover=False, - force_tags=False): + set_authors=True, commit=True, force_changes=False): ''' Set metadata for the book `id` from the `Metadata` object `mi` + + Setting force_changes=True will force set_metadata to update fields even + if mi contains empty values. In this case, 'None' is distinguished from + 'empty'. If mi.XXX is None, the XXX is not replaced, otherwise it is. + The tags, identifiers, and cover attributes are special cases. Tags and + identifiers cannot be set to None so then will always be replaced if + force_changes is true. You must ensure that mi contains the values you + want the book to have. Covers are always changed if a new cover is + provided, but are never deleted. Also note that force_changes has no + effect on setting title or authors. ''' if callable(getattr(mi, 'to_book_metadata', None)): # Handle code passing in a OPF object instead of a Metadata object @@ -1708,12 +1717,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): traceback.print_exc() else: raise - # force_changes has no role to play in setting title or author + + def should_replace_field(attr): + return (force_changes and (mi.get(attr, None) is not None)) or \ + not mi.is_null(attr) + path_changed = False - if set_title and not mi.is_null('title'): + if set_title and mi.title: self._set_title(id, mi.title) path_changed = True - if set_authors and not mi.is_null('authors'): + if set_authors: + if not mi.authors: + mi.authors = [_('Unknown')] authors = [] for a in mi.authors: authors += string_to_authors(a) @@ -1722,17 +1737,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if path_changed: self.set_path(id, index_is_id=True) - if not mi.is_null('author_sort'): + if should_replace_field('author_sort'): doit(self.set_author_sort, id, mi.author_sort, notify=False, commit=False) - if not mi.is_null('publisher'): + if should_replace_field('publisher'): doit(self.set_publisher, id, mi.publisher, notify=False, commit=False) - if not mi.is_null('rating'): + + # Setting rating to zero is acceptable. + if mi.rating is not None: doit(self.set_rating, id, mi.rating, notify=False, commit=False) - if not mi.is_null('series'): + if should_replace_field('series'): doit(self.set_series, id, mi.series, notify=False, commit=False) + # force_changes has no effect on cover manipulation if mi.cover_data[1] is not None: doit(self.set_cover, id, mi.cover_data[1], commit=False) elif mi.cover is not None: @@ -1741,36 +1759,45 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): raw = f.read() if raw: doit(self.set_cover, id, raw, commit=False) - elif force_cover: - doit(self.remove_cover, id, notify=False, commit=False) - if force_tags or not mi.is_null('tags'): + # if force_changes is true, tags are always replaced because the + # attribute cannot be set to None. + if should_replace_field('tags'): doit(self.set_tags, id, mi.tags, notify=False, commit=False) - if not mi.is_null('comments'): + + if should_replace_field('comments'): doit(self.set_comment, id, mi.comments, notify=False, commit=False) - if not mi.is_null('series_index'): + + # Setting series_index to zero is acceptable + if mi.series_index is not None: doit(self.set_series_index, id, mi.series_index, notify=False, commit=False) - if not mi.is_null('pubdate'): + if should_replace_field('pubdate'): doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False) if getattr(mi, 'timestamp', None) is not None: doit(self.set_timestamp, id, mi.timestamp, notify=False, commit=False) + # identifiers will always be replaced if force_changes is True mi_idents = mi.get_identifiers() - if mi_idents: + if force_changes: + self.set_identifiers(id, mi_idents, notify=False, commit=False) + elif mi_idents: identifiers = self.get_identifiers(id, index_is_id=True) for key, val in mi_idents.iteritems(): - if val and val.strip(): + if val and val.strip(): # Don't delete an existing identifier identifiers[icu_lower(key)] = val self.set_identifiers(id, identifiers, notify=False, commit=False) + user_mi = mi.get_all_user_metadata(make_copy=False) for key in user_mi.iterkeys(): if key in self.field_metadata and \ user_mi[key]['datatype'] == self.field_metadata[key]['datatype']: - doit(self.set_custom, id, val=mi.get(key), commit=False, - extra=mi.get_extra(key), label=user_mi[key]['label']) + val = mi.get(key, None) + if force_changes or val is not None: + doit(self.set_custom, id, val=val, extra=mi.get_extra(key), + label=user_mi[key]['label'], commit=False) if commit: self.conn.commit() self.notify('metadata', [id]) From 85214e226355a520f1f8432d2b13376ea021ad00 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 13 Mar 2011 13:28:01 +0000 Subject: [PATCH 08/41] Make searches in the tag browser a possible hierarchical field. --- .../gui2/dialogs/saved_search_editor.py | 32 ++++++++++++++++--- .../gui2/dialogs/saved_search_editor.ui | 14 ++++++++ src/calibre/gui2/preferences/look_feel.py | 1 + src/calibre/gui2/tag_view.py | 20 ++++++++---- src/calibre/library/database2.py | 2 +- 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/calibre/gui2/dialogs/saved_search_editor.py b/src/calibre/gui2/dialogs/saved_search_editor.py index 1143a6f06a..c9f843109a 100644 --- a/src/calibre/gui2/dialogs/saved_search_editor.py +++ b/src/calibre/gui2/dialogs/saved_search_editor.py @@ -9,12 +9,13 @@ from PyQt4.QtGui import QDialog from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor from calibre.utils.search_query_parser import saved_searches from calibre.utils.icu import sort_key +from calibre.gui2 import error_dialog from calibre.gui2.dialogs.confirm_delete import confirm class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): - def __init__(self, window, initial_search=None): - QDialog.__init__(self, window) + def __init__(self, parent, initial_search=None): + QDialog.__init__(self, parent) Ui_SavedSearchEditor.__init__(self) self.setupUi(self) @@ -22,12 +23,13 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'), self.current_index_changed) self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search) + self.rename_button.clicked.connect(self.rename_search) self.current_search_name = None self.searches = {} - self.searches_to_delete = [] for name in saved_searches().names(): self.searches[name] = saved_searches().lookup(name) + self.search_names = set([icu_lower(n) for n in saved_searches().names()]) self.populate_search_list() if initial_search is not None and initial_search in self.searches: @@ -42,6 +44,11 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): search_name = unicode(self.input_box.text()).strip() if search_name == '': return False + if icu_lower(search_name) in self.search_names: + error_dialog(self, _('Saved search already exists'), + _('The saved search %s already exists, perhaps with ' + 'different case')%search_name).exec_() + return False if search_name not in self.searches: self.searches[search_name] = '' self.populate_search_list() @@ -57,10 +64,25 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): +'

', 'saved_search_editor_delete', self): return del self.searches[self.current_search_name] - self.searches_to_delete.append(self.current_search_name) self.current_search_name = None self.search_name_box.removeItem(self.search_name_box.currentIndex()) + def rename_search(self): + new_search_name = unicode(self.input_box.text()).strip() + if new_search_name == '': + return False + if icu_lower(new_search_name) in self.search_names: + error_dialog(self, _('Saved search already exists'), + _('The saved search %s already exists, perhaps with ' + 'different case')%new_search_name).exec_() + return False + if self.current_search_name in self.searches: + self.searches[new_search_name] = self.searches[self.current_search_name] + del self.searches[self.current_search_name] + self.populate_search_list() + self.select_search(new_search_name) + return True + def select_search(self, name): self.search_name_box.setCurrentIndex(self.search_name_box.findText(name)) @@ -78,7 +100,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): def accept(self): if self.current_search_name: self.searches[self.current_search_name] = unicode(self.search_text.toPlainText()) - for name in self.searches_to_delete: + for name in saved_searches().names(): saved_searches().delete(name) for name in self.searches: saved_searches().add(name, self.searches[name]) diff --git a/src/calibre/gui2/dialogs/saved_search_editor.ui b/src/calibre/gui2/dialogs/saved_search_editor.ui index 3ba37bdf10..99672b5b8e 100644 --- a/src/calibre/gui2/dialogs/saved_search_editor.ui +++ b/src/calibre/gui2/dialogs/saved_search_editor.ui @@ -134,6 +134,20 @@ + + + + Rename the current search to what is in the box + + + ... + + + + :/images/edit-undo.png:/images/edit-undo.png + + + diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 15d5666978..206f2b97fb 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -67,6 +67,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): if db.field_metadata[k]['is_category'] and db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']]) choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers']) + choices |= set(['search']) self.opt_categories_using_hierarchy.update_items_cache(choices) r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList, choices=sorted(list(choices), key=sort_key)) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index c4871880a4..12a29a469c 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -533,7 +533,9 @@ class TagsView(QTreeView): # {{{ self.setModel(self._model) except: # The DB must be gone. Set the model to None and hope that someone - # will call set_database later. I don't know if this in fact works + # will call set_database later. I don't know if this in fact works. + # But perhaps a Bad Thing Happened, so print the exception + traceback.print_exc() self._model = None self.setModel(None) # }}} @@ -678,7 +680,8 @@ class TagTreeItem(object): # {{{ break elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\ self.tag.state == TAG_SEARCH_STATES['mark_minusminus']: - if self.tag.is_hierarchical and len(self.children): + if self.tag.is_searchable and self.tag.is_hierarchical \ + and len(self.children): break else: break @@ -1258,19 +1261,22 @@ class TagsModel(QAbstractItemModel): # {{{ if t.type != TagTreeItem.CATEGORY]) if (comp,tag.category) in child_map: node_parent = child_map[(comp,tag.category)] - node_parent.tag.is_hierarchical = True + node_parent.tag.is_hierarchical = key != 'search' else: if i < len(components)-1: t = copy.copy(tag) t.original_name = '.'.join(components[:i+1]) - # This 'manufactured' intermediate node can - # be searched, but cannot be edited. - t.is_editable = False + if key != 'search': + # This 'manufactured' intermediate node can + # be searched, but cannot be edited. + t.is_editable = False + else: + t.is_searchable = t.is_editable = False else: t = tag if not in_uc: t.original_name = t.name - t.is_hierarchical = True + t.is_hierarchical = key != 'search' t.name = comp self.beginInsertRows(category_index, 999999, 1) node_parent = TagTreeItem(parent=node_parent, data=t, diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index ea30c08aff..e70a746b15 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -56,7 +56,7 @@ class Tag(object): self.is_hierarchical = False self.is_editable = is_editable self.is_searchable = is_searchable - self.id_set = id_set + self.id_set = id_set if id_set is not None else set([]) self.avg_rating = avg/2.0 if avg is not None else 0 self.sort = sort if self.avg_rating > 0: From 1cc3038f0ed2f5986bd013fcbcc6d7cbd45d76eb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 13 Mar 2011 10:08:38 -0600 Subject: [PATCH 09/41] Update Draw And Cook --- resources/images/news/DrawAndCook.png | Bin 0 -> 575 bytes resources/recipes/DrawAndCook.recipe | 21 +++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 resources/images/news/DrawAndCook.png diff --git a/resources/images/news/DrawAndCook.png b/resources/images/news/DrawAndCook.png new file mode 100644 index 0000000000000000000000000000000000000000..8b40b7534407f55bb439e045effdc557791a81ce GIT binary patch literal 575 zcmV-F0>J%=P)|KG3o-a>;04I2E9@J4sM(FMSsH@X9qz_IL~$+ZBEfHm+4 z3}k%`tdw<+HNn3EJ!M@xv-b%YCvm&)ywSfhaUK#8r@$?+lV|&~-&NM#RWk$b<@S9l z(i?p#N$h|Puu#^mTEGP`1U|?QV5zKYE9(vtlDV?(Kr$$R??RYjnykx;D+Az4Vs@2v zD}a`=ZltVRD(g1MpTA{X0YF(d7gqo-6Osbx0aq1Q&VX}Zt*rZ4jn}H&7joTZxSvId zD|6r^7L$;ZA&IVxatT!h97~X#fI30Z>M^iP_6s?p6N2^Gn0s#^szXPvTOm3EV)i#c zpWlvN*9ECNg1cE*5pRWpe_`>{~A}1rC9cWIvGSp9#*y&e^^e zs4C!9$S^r4oa>5Ed^=z$Ep`e_r5!?y}};Xu;s(cXoS$MYT&=EgD;zeA{pfj4@Z z&JAyL`G)}6+^>+vAP}MBE)=U@d6Sph1IwjVH<+zROZZOUM8K N002ovPDHLkV1im81SkLi literal 0 HcmV?d00001 diff --git a/resources/recipes/DrawAndCook.recipe b/resources/recipes/DrawAndCook.recipe index 1c080b85db..8db4f71014 100644 --- a/resources/recipes/DrawAndCook.recipe +++ b/resources/recipes/DrawAndCook.recipe @@ -1,8 +1,11 @@ from calibre.web.feeds.news import BasicNewsRecipe +import re class DrawAndCook(BasicNewsRecipe): title = 'DrawAndCook' __author__ = 'Starson17' + __version__ = 'v1.10' + __date__ = '13 March 2011' description = 'Drawings of recipes!' language = 'en' publisher = 'Starson17' @@ -13,6 +16,7 @@ class DrawAndCook(BasicNewsRecipe): remove_javascript = True remove_empty_feeds = True cover_url = 'http://farm5.static.flickr.com/4043/4471139063_4dafced67f_o.jpg' + INDEX = 'http://www.theydrawandcook.com' max_articles_per_feed = 30 remove_attributes = ['style', 'font'] @@ -34,20 +38,21 @@ class DrawAndCook(BasicNewsRecipe): date = '' current_articles = [] soup = self.index_to_soup(url) - recipes = soup.findAll('div', attrs={'class': 'date-outer'}) + featured_major_slider = soup.find(name='div', attrs={'id':'featured_major_slider'}) + recipes = featured_major_slider.findAll('li', attrs={'data-id': re.compile(r'artwork_entry_\d+', re.DOTALL)}) for recipe in recipes: - title = recipe.h3.a.string - page_url = recipe.h3.a['href'] + page_url = self.INDEX + recipe.a['href'] + print 'page_url is: ', page_url + title = recipe.find('strong').string + print 'title is: ', title current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':date}) return current_articles - - keep_only_tags = [dict(name='h3', attrs={'class':'post-title entry-title'}) - ,dict(name='div', attrs={'class':'post-body entry-content'}) + keep_only_tags = [dict(name='h1', attrs={'id':'page_title'}) + ,dict(name='section', attrs={'id':'artwork'}) ] - remove_tags = [dict(name='div', attrs={'class':['separator']}) - ,dict(name='div', attrs={'class':['post-share-buttons']}) + remove_tags = [dict(name='article', attrs={'id':['recipe_actions', 'metadata']}) ] extra_css = ''' From 2f6a09e10e77c181512524f5ca13fabc6e4bffa9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Mar 2011 10:07:07 +0000 Subject: [PATCH 10/41] Fix #9395: Ondevice status not updated, regenerated --- src/calibre/devices/usbms/driver.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index a19df07abf..578c28b894 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -10,7 +10,7 @@ driver. It is intended to be subclassed with the relevant parts implemented for a particular device. ''' -import os, re, time, json, uuid +import os, re, time, json, uuid, functools from itertools import cycle from calibre.constants import numeric_version @@ -372,15 +372,21 @@ class USBMS(CLI, Device): @classmethod def build_template_regexp(cls): - def replfunc(match): - if match.group(1) in ['title', 'series', 'series_index', 'isbn']: - return '(?P<' + match.group(1) + '>.+?)' - elif match.group(1) in ['authors', 'author_sort']: - return '(?P.+?)' - else: - return '(.+?)' + def replfunc(match, seen=None): + v = match.group(1) + if v in ['title', 'series', 'series_index', 'isbn']: + if v not in seen: + seen |= set([v]) + return '(?P<' + v + '>.+?)' + elif v in ['authors', 'author_sort']: + if v not in seen: + seen |= set([v]) + return '(?P.+?)' + return '(.+?)' + s = set() + f = functools.partial(replfunc, seen=s) template = cls.save_template().rpartition('/')[2] - return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)') + return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') @classmethod def path_to_unicode(cls, path): From 4b7575252eb184dff028866ad7d7191df324cdfc Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Mar 2011 10:41:52 +0000 Subject: [PATCH 11/41] #9397: Search Replace error on custom series_index field --- src/calibre/gui2/dialogs/metadata_bulk.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index d918991aad..9b25545252 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -783,6 +783,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): books_to_refresh = self.db.set_custom(id, val, label=dfm['label'], extra=extra, commit=False, allow_case_change=True) + elif dest.startswith('#') and dest.endswith('_index'): + label = self.db.field_metadata[dest[:-6]]['label'] + series = self.db.get_custom(id, label=label, index_is_id=True) + books_to_refresh = self.db.set_custom(id, series, label=label, + extra=val, commit=False, + allow_case_change=True) else: if dest == 'comments': setter = self.db.set_comment From 028d2fc04b261db1da9f292c3e6606109f202714 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Mar 2011 12:19:57 +0000 Subject: [PATCH 12/41] Currently searches for '..X' match only items exactly equal to '.X'. Change to make such a search match that and also any exactly-matching embedded hierarchical components, such as 'A.X' and 'A.X.Z'. This means that one cannot search for *only* '.X' if there is a hierarchical subitem with the name X, but I think this case will be exceedingly rare. --- src/calibre/library/caches.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 97ddaeb51a..119a5492e0 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -123,14 +123,23 @@ REGEXP_MATCH = 2 def _match(query, value, matchkind): if query.startswith('..'): query = query[1:] - prefix_match_ok = False + sq = query[1:] + internal_match_ok = True else: - prefix_match_ok = True + internal_match_ok = False for t in value: t = icu_lower(t) try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished if (matchkind == EQUALS_MATCH): - if prefix_match_ok and query[0] == '.': + if internal_match_ok: + print query, t + if query == t: + return True + comps = [c.strip() for c in t.split('.') if c.strip()] + for comp in comps: + if sq == comp: + return True + elif query[0] == '.': if t.startswith(query[1:]): ql = len(query) - 1 if (len(t) == ql) or (t[ql:ql+1] == '.'): From 43fe7d047f4eddf457dff1f03b5e3ab2a5b65424 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Mar 2011 13:02:05 +0000 Subject: [PATCH 13/41] ... --- src/calibre/library/caches.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 119a5492e0..be996063d5 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -132,7 +132,6 @@ def _match(query, value, matchkind): try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished if (matchkind == EQUALS_MATCH): if internal_match_ok: - print query, t if query == t: return True comps = [c.strip() for c in t.split('.') if c.strip()] From 1acb8b2fca2fe37af7ea1e81f6018eaf20ed6503 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 14 Mar 2011 09:46:33 -0600 Subject: [PATCH 14/41] Updated rbc.ru --- resources/recipes/rbc_ru.recipe | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/resources/recipes/rbc_ru.recipe b/resources/recipes/rbc_ru.recipe index 4c377a334b..2495a195dc 100644 --- a/resources/recipes/rbc_ru.recipe +++ b/resources/recipes/rbc_ru.recipe @@ -1,24 +1,25 @@ +# -*- coding: utf-8 -*- + from calibre.web.feeds.news import BasicNewsRecipe -class AdvancedUserRecipe1286819935(BasicNewsRecipe): +class RBC_ru(BasicNewsRecipe): title = u'RBC.ru' __author__ = 'A. Chewi' - oldest_article = 7 - max_articles_per_feed = 100 + description = u'Российское информационное агентство «РосБизнесКонсалтинг» (РБК) - ленты новостей политики, экономики и финансов, аналитические материалы, комментарии и прогнозы, тематические статьи' + needs_subscription = False + cover_url = 'http://pics.rbc.ru/img/fp_v4/skin/img/logo.gif' + cover_margins = (80, 160, '#ffffff') + oldest_article = 10 + max_articles_per_feed = 50 + summary_length = 200 + remove_empty_feeds = True no_stylesheets = True + remove_javascript = True use_embedded_content = False conversion_options = {'linearize_tables' : True} - remove_attributes = ['style'] language = 'ru' timefmt = ' [%a, %d %b, %Y]' - keep_only_tags = [dict(name='h2', attrs={}), - dict(name='div', attrs={'class': 'box _ga1_on_'}), - dict(name='h1', attrs={'class': 'news_section'}), - dict(name='div', attrs={'class': 'news_body dotted_border_bottom'}), - dict(name='table', attrs={'class': 'newsBody'}), - dict(name='h2', attrs={'class': 'black'})] - feeds = [(u'Главные новости', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/rbc.ru/mainnews.rss'), (u'Политика', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/rbc.ru/politics.rss'), (u'Экономика', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/rbc.ru/economics.rss'), @@ -26,6 +27,12 @@ class AdvancedUserRecipe1286819935(BasicNewsRecipe): (u'Происшествия', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/rbc.ru/incidents.rss'), (u'Финансовые новости Quote.rbc.ru', u'http://static.feed.rbc.ru/rbc/internal/rss.rbc.ru/quote.ru/mainnews.rss')] + keep_only_tags = [dict(name='h2', attrs={}), + dict(name='div', attrs={'class': 'box _ga1_on_'}), + dict(name='h1', attrs={'class': 'news_section'}), + dict(name='div', attrs={'class': 'news_body dotted_border_bottom'}), + dict(name='table', attrs={'class': 'newsBody'}), + dict(name='h2', attrs={'class': 'black'})] remove_tags = [dict(name='div', attrs={'class': "video-frame"}), dict(name='div', attrs={'class': "photo-container videoContainer videoSWFLinks videoPreviewSlideContainer notes"}), From 4fa75a4b4a3df79fef8959fb15d2731e0c4e6057 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 14 Mar 2011 09:58:54 -0600 Subject: [PATCH 15/41] ... --- src/calibre/devices/__init__.py | 4 ++-- src/calibre/devices/prs505/driver.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py index 1918a36cc8..63b0b89a17 100644 --- a/src/calibre/devices/__init__.py +++ b/src/calibre/devices/__init__.py @@ -47,7 +47,7 @@ def get_connected_device(): for d in connected_devices: try: - d.open() + d.open(None) except: continue else: @@ -121,7 +121,7 @@ def debug(ioreg_to_tmp=False, buf=None): out('Trying to open', dev.name, '...', end=' ') try: dev.reset(detected_device=det) - dev.open() + dev.open(None) out('OK') except: import traceback diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 9f17ea22a4..da1ef55786 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -221,7 +221,8 @@ class PRS505(USBMS): os.path.splitext(os.path.basename(p))[0], book, p) except: - debug_print('FAILED to upload cover', p) + debug_print('FAILED to upload cover', + prefix, book.lpath) else: debug_print('PRS505: NOT uploading covers in sync_booklists') From b9744d6f26be95bf4f0f98832782a6f116777219 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 14 Mar 2011 10:12:01 -0600 Subject: [PATCH 16/41] ... --- src/calibre/devices/android/driver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 81d35cc159..0491f34d78 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -48,6 +48,7 @@ class ANDROID(USBMS): 0x04e8 : { 0x681d : [0x0222, 0x0223, 0x0224, 0x0400], 0x681c : [0x0222, 0x0224, 0x0400], 0x6640 : [0x0100], + 0x6877 : [0x0400], }, # Acer From 9d034bdc23c82f999854c718142a5dc1b9ad248c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Mar 2011 19:17:42 +0000 Subject: [PATCH 17/41] Fix dev.open in debug device detection --- src/calibre/devices/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py index 1918a36cc8..63b0b89a17 100644 --- a/src/calibre/devices/__init__.py +++ b/src/calibre/devices/__init__.py @@ -47,7 +47,7 @@ def get_connected_device(): for d in connected_devices: try: - d.open() + d.open(None) except: continue else: @@ -121,7 +121,7 @@ def debug(ioreg_to_tmp=False, buf=None): out('Trying to open', dev.name, '...', end=' ') try: dev.reset(detected_device=det) - dev.open() + dev.open(None) out('OK') except: import traceback From d76a0e9c90a7b3c9eb320e6e5dfe3a93307910c3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 15 Mar 2011 14:13:54 +0000 Subject: [PATCH 18/41] Add rename and delete search to tag browser context menu --- src/calibre/gui2/search_box.py | 7 +++++-- src/calibre/gui2/tag_view.py | 25 +++++++++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 5b8501ebb5..fa3b597636 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -453,8 +453,11 @@ class SavedSearchBoxMixin(object): # {{{ d = SavedSearchEditor(self, search) d.exec_() if d.result() == d.Accepted: - self.saved_searches_changed() - self.saved_search.clear() + self.do_rebuild_saved_searches() + + def do_rebuild_saved_searches(self): + self.saved_searches_changed() + self.saved_search.clear() # }}} diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 12a29a469c..e56c02d43f 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -81,6 +81,7 @@ class TagsView(QTreeView): # {{{ add_subcategory = pyqtSignal(object) tag_list_edit = pyqtSignal(object, object) saved_search_edit = pyqtSignal(object) + rebuild_saved_searches = pyqtSignal() author_sort_edit = pyqtSignal(object, object) tag_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal() @@ -111,6 +112,8 @@ class TagsView(QTreeView): # {{{ self.collapse_model = gprefs['tags_browser_partition_method'] self.search_icon = QIcon(I('search.png')) self.user_category_icon = QIcon(I('tb_folder.png')) + self.delete_icon = QIcon(I('list_remove.png')) + self.rename_icon = QIcon(I('edit-undo.png')) def set_pane_is_visible(self, to_what): pv = self.pane_is_visible @@ -251,6 +254,10 @@ class TagsView(QTreeView): # {{{ if action == 'delete_user_category': self.delete_user_category.emit(key) return + if action == 'delete_search': + saved_searches().delete(key) + self.rebuild_saved_searches.emit() + return if action == 'delete_item_from_user_category': tag = index.tag if len(index.children) > 0: @@ -313,7 +320,8 @@ class TagsView(QTreeView): # {{{ # the possibility of renaming that item. if tag.is_editable: # Add the 'rename' items - self.context_menu.addAction(_('Rename %s')%tag.name, + self.context_menu.addAction(self.rename_icon, + _('Rename %s')%tag.name, partial(self.context_menu_handler, action='edit_item', index=index)) if key == 'authors': @@ -341,7 +349,15 @@ class TagsView(QTreeView): # {{{ add_node_tree(tree_dict[k], tm, p) p.pop() add_node_tree(nt, m, []) - + elif key == 'search': + self.context_menu.addAction(self.rename_icon, + _('Rename %s')%tag.name, + partial(self.context_menu_handler, action='edit_item', + index=index)) + self.context_menu.addAction(self.delete_icon, + _('Delete search %s')%tag.name, + partial(self.context_menu_handler, + action='delete_search', key=tag.name)) if key.startswith('@') and not item.is_gst: self.context_menu.addAction(self.user_category_icon, _('Remove %s from category %s')%(tag.name, item.py_name), @@ -362,7 +378,7 @@ class TagsView(QTreeView): # {{{ self.context_menu.addSeparator() elif key.startswith('@') and not item.is_gst: if item.can_be_edited: - self.context_menu.addAction(self.user_category_icon, + self.context_menu.addAction(self.rename_icon, _('Rename %s')%item.py_name, partial(self.context_menu_handler, action='edit_item', index=index)) @@ -370,7 +386,7 @@ class TagsView(QTreeView): # {{{ _('Add sub-category to %s')%item.py_name, partial(self.context_menu_handler, action='add_subcategory', key=key)) - self.context_menu.addAction(self.user_category_icon, + self.context_menu.addAction(self.delete_icon, _('Delete user category %s')%item.py_name, partial(self.context_menu_handler, action='delete_user_category', key=key)) @@ -1768,6 +1784,7 @@ class TagBrowserMixin(object): # {{{ self.tags_view.add_subcategory.connect(self.do_add_subcategory) self.tags_view.add_item_to_user_cat.connect(self.do_add_item_to_user_cat) self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) + self.tags_view.rebuild_saved_searches.connect(self.do_rebuild_saved_searches) self.tags_view.author_sort_edit.connect(self.do_author_sort_edit) self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed) self.tags_view.search_item_renamed.connect(self.saved_searches_changed) From c89cb85f12a1642490e7cb58928085056da98891 Mon Sep 17 00:00:00 2001 From: Timothy Legge Date: Tue, 15 Mar 2011 11:39:22 -0300 Subject: [PATCH 19/41] Add support for Closed Collection to Kobo --- src/calibre/devices/kobo/driver.py | 40 +++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index f1c0d3f3d3..3a0254ef20 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -115,6 +115,8 @@ class KOBO(USBMS): playlist_map[lpath]= "Im_Reading" elif readstatus == 2: playlist_map[lpath]= "Read" + elif readstatus == 3: + playlist_map[lpath]= "Closed" path = self.normalize_path(path) # print "Normalized FileName: " + path @@ -599,11 +601,47 @@ class KOBO(USBMS): try: cursor.execute('update content set ReadStatus=2,FirstTimeReading=\'true\' where BookID is Null and ContentID = ?', t) except: - debug_print('Database Exception: Unable set book as Rinished') + debug_print('Database Exception: Unable set book as Finished') raise else: connection.commit() # debug_print('Database: Commit set ReadStatus as Finished') + if category == 'Closed': + # Reset Im_Reading list in the database + if oncard == 'carda': + query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ReadStatus = 3 and ContentID like \'file:///mnt/sd/%\'' + elif oncard != 'carda' and oncard != 'cardb': + query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ReadStatus = 3 and ContentID not like \'file:///mnt/sd/%\'' + + try: + cursor.execute (query) + except: + debug_print('Database Exception: Unable to reset Closed list') + raise + else: +# debug_print('Commit: Reset Closed list') + connection.commit() + + for book in books: +# debug_print('Title:', book.title, 'lpath:', book.path) + book.device_collections = ['Closed'] + + extension = os.path.splitext(book.path)[1] + ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path) + + ContentID = self.contentid_from_path(book.path, ContentType) +# datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) + + t = (ContentID,) + + try: + cursor.execute('update content set ReadStatus=3,FirstTimeReading=\'true\' where BookID is Null and ContentID = ?', t) + except: + debug_print('Database Exception: Unable set book as Closed') + raise + else: + connection.commit() +# debug_print('Database: Commit set ReadStatus as Closed') else: # No collections # Since no collections exist the ReadStatus needs to be reset to 0 (Unread) print "Reseting ReadStatus to 0" From ef1f808d5b30baefa2a63026265284451703b577 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Mar 2011 10:50:23 -0600 Subject: [PATCH 20/41] Office Space and Modoros by Zsolt Botykai --- resources/recipes/modoros.recipe | 89 +++++++++++++++++++++ resources/recipes/office_space.recipe | 109 ++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 resources/recipes/modoros.recipe create mode 100644 resources/recipes/office_space.recipe diff --git a/resources/recipes/modoros.recipe b/resources/recipes/modoros.recipe new file mode 100644 index 0000000000..72980298d6 --- /dev/null +++ b/resources/recipes/modoros.recipe @@ -0,0 +1,89 @@ +import re +from calibre.web.feeds.recipes import BasicNewsRecipe +from calibre.constants import config_dir, CONFIG_DIR_MODE +import os, os.path, urllib +from hashlib import md5 + +class ModorosBlogHu(BasicNewsRecipe): + __author__ = 'Zsolt Botykai' + title = u'Modoros Blog' + description = u"Modoros.blog.hu" + oldest_article = 10000 + max_articles_per_feed = 10000 + reverse_article_order = True + language = 'hu' + remove_javascript = True + remove_empty_feeds = True + no_stylesheets = True + feeds = [(u'Modoros Blog', u'http://modoros.blog.hu/rss')] + remove_javascript = True + use_embedded_content = False + preprocess_regexps = [ + (re.compile(r'.*?', re.DOTALL|re.IGNORECASE), + lambda match: ''), + (re.compile(r'

', re.DOTALL|re.IGNORECASE), lambda m: ''), + (re.compile(r'( | )*?

', re.DOTALL|re.IGNORECASE), lambda match: ''), + (re.compile(r']+>.*?
.*?', re.DOTALL|re.IGNORECASE), lambda match: ''), + (re.compile(r'