From 2cb241b205224202f01b452ed7310fca302c62cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Mon, 23 May 2011 22:56:39 +0200 Subject: [PATCH 01/30] legimi store --- src/calibre/customize/builtins.py | 11 ++++ src/calibre/gui2/store/legimi_plugin.py | 75 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/calibre/gui2/store/legimi_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 680e36d0f3..d9e8be00b5 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1272,6 +1272,16 @@ class StoreKoboStore(StoreBase): headquarters = 'CA' formats = ['EPUB'] +class StoreLegimiStore(StoreBase): + name = 'Legimi' + author = u'Tomasz Długosz' + description = u'Tanie oraz darmowe ebooki, egazety i blogi w formacie EPUB, wprost na Twój e-czytnik, iPhone, iPad, Android i komputer' + actual_plugin = 'calibre.gui2.store.legimi_plugin:LegimiStore' + + drm_free_only = False + headquarters = 'PL' + formats = ['EPUB'] + class StoreManyBooksStore(StoreBase): name = 'ManyBooks' description = u'Public domain and creative commons works from many sources.' @@ -1393,6 +1403,7 @@ plugins += [ StoreGoogleBooksStore, StoreGutenbergStore, StoreKoboStore, + StoreLegimiStore, StoreManyBooksStore, StoreMobileReadStore, StoreNextoStore, diff --git a/src/calibre/gui2/store/legimi_plugin.py b/src/calibre/gui2/store/legimi_plugin.py new file mode 100644 index 0000000000..7212f0f394 --- /dev/null +++ b/src/calibre/gui2/store/legimi_plugin.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, Tomasz Długosz ' +__docformat__ = 'restructuredtext en' + +import re +import urllib +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class LegimiStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + + url = 'http://www.legimi.com/pl/ebooks/?price=any' + detail_url = None + + if detail_item: + detail_url = detail_item + + if external or self.config.get('open_external', False): + open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(self.config.get('tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.legimi.com/pl/ebooks/?price=any&lang=pl&search=' + urllib.quote_plus(query.encode('utf-8')) + '&sort=relevance' + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@class="list"]/ul/li'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//div[@class="item_cover_container"]/a[1]/@href')) + if not id: + continue + + cover_url = ''.join(data.xpath('.//div[@class="item_cover_container"]/a/img/@src')) + title = ''.join(data.xpath('.//div[@class="item_entries"]/h2/a/text()')) + author = ''.join(data.xpath('.//div[@class="item_entries"]/span[1]/a/text()')) + price = ''.join(data.xpath('.//div[@class="item_entries"]/span[3]/text()')) + price = re.sub(r'[^0-9,]*','',price) + ' zł' + + counter -= 1 + + s = SearchResult() + s.cover_url = 'http://www.legimi.com/' + cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price + s.detail_item = 'http://www.legimi.com/' + id.strip() + s.drm = SearchResult.DRM_LOCKED + s.formats = 'EPUB' + + yield s From 99ad2c86b5cd71541b2f9370700ee14ec908c7f3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 May 2011 08:23:50 +0100 Subject: [PATCH 02/30] Small change to help text. Add tooltip to wizard button. Have tag column expand in tag wizard when resizing dialog. --- src/calibre/gui2/dialogs/template_line_editor.py | 1 + src/calibre/gui2/preferences/look_feel.py | 4 ++-- src/calibre/gui2/preferences/look_feel.ui | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py index 95727e442b..26f4dd0753 100644 --- a/src/calibre/gui2/dialogs/template_line_editor.py +++ b/src/calibre/gui2/dialogs/template_line_editor.py @@ -63,6 +63,7 @@ class TagWizard(QDialog): self.tags = tags l = QGridLayout() self.setLayout(l) + l.setColumnStretch(0, 1) l.addWidget(QLabel(_('Tag Value')), 0, 0, 1, 1) l.addWidget(QLabel(_('Color')), 0, 1, 1, 1) self.tagboxes = [] diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 483934825d..49bfb1df1a 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -167,8 +167,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): '' 'tutorial on using templates.') + '

' + - _('If you want to color a field based on tags, then right-click ' - 'in an empty template line and choose tags wizard. ' + _('If you want to color a field based on tags, then click the ' + 'button next to an empty line to open the tags wizard. ' 'It will build a template for you. You can later edit that ' 'template with the same wizard. If you edit it by hand, the ' 'wizard might not work or might restore old values.') + diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index a67a3585cb..fe6134f235 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -442,6 +442,9 @@ then the tags will be displayed each on their own line. :/images/wizard.png:/images/wizard.png + + Open the tags wizard. + @@ -456,6 +459,9 @@ then the tags will be displayed each on their own line. :/images/wizard.png:/images/wizard.png + + Open the tags wizard. + @@ -470,6 +476,9 @@ then the tags will be displayed each on their own line. :/images/wizard.png:/images/wizard.png + + Open the tags wizard. + @@ -484,6 +493,9 @@ then the tags will be displayed each on their own line. :/images/wizard.png:/images/wizard.png + + Open the tags wizard. + @@ -498,6 +510,9 @@ then the tags will be displayed each on their own line. :/images/wizard.png:/images/wizard.png + + Open the tags wizard. + From 1bb4911ece2374f3fc2fce0e57383fc11f847e8c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 24 May 2011 10:54:06 -0600 Subject: [PATCH 03/30] ... --- src/calibre/linux.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 1e7a62b869..9e58d4f638 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -356,7 +356,7 @@ class PostInstall: mimetypes = set([]) for x in all_input_formats(): mt = guess_type('dummy.'+x)[0] - if mt and 'chemical' not in mt: + if mt and 'chemical' not in mt and 'ctc-posml' not in mt: mimetypes.add(mt) def write_mimetypes(f): @@ -376,11 +376,10 @@ class PostInstall: des = ('calibre-gui.desktop', 'calibre-lrfviewer.desktop', 'calibre-ebook-viewer.desktop') for x in des: - cmd = ['xdg-desktop-menu', 'install', './'+x] - if x != des[-1]: - cmd.insert(2, '--noupdate') + cmd = ['xdg-desktop-menu', 'install', '--noupdate', './'+x] check_call(' '.join(cmd), shell=True) self.menu_resources.append(x) + check_call(['xdg-desktop-menu', 'forceupdate']) f = open('calibre-mimetypes', 'wb') f.write(MIME) f.close() From 268a81499cde8ae38063679343306113148b158a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 May 2011 18:26:04 +0100 Subject: [PATCH 04/30] Change control to a line edit. Improve generated template code. --- .../gui2/dialogs/template_line_editor.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py index 26f4dd0753..98b74b391d 100644 --- a/src/calibre/gui2/dialogs/template_line_editor.py +++ b/src/calibre/gui2/dialogs/template_line_editor.py @@ -9,7 +9,7 @@ from PyQt4.Qt import (QLineEdit, QDialog, QGridLayout, QLabel, QDialogButtonBox, QColor, QComboBox, QIcon) from calibre.gui2.dialogs.template_dialog import TemplateDialog -from calibre.gui2.complete import MultiCompleteComboBox +from calibre.gui2.complete import MultiCompleteLineEdit from calibre.gui2 import error_dialog class TemplateLineEditor(QLineEdit): @@ -64,14 +64,15 @@ class TagWizard(QDialog): l = QGridLayout() self.setLayout(l) l.setColumnStretch(0, 1) - l.addWidget(QLabel(_('Tag Value')), 0, 0, 1, 1) + l.setColumnMinimumWidth(0, 300) + l.addWidget(QLabel(_('Tags (more than one per box permitted)')), 0, 0, 1, 1) l.addWidget(QLabel(_('Color')), 0, 1, 1, 1) self.tagboxes = [] self.colorboxes = [] self.colors = [unicode(s) for s in list(QColor.colorNames())] self.colors.insert(0, '') for i in range(0, 10): - tb = MultiCompleteComboBox(self) + tb = MultiCompleteLineEdit(self) tb.set_separator(', ') tb.update_items_cache(self.tags) self.tagboxes.append(tb) @@ -102,10 +103,11 @@ class TagWizard(QDialog): def accepted(self): res = ("program:\n#tag wizard -- do not directly edit\n" - " t = field('tags');\n first_non_empty(\n") + " t = field('tags');\n first_non_empty(\n") lines = [] for tb, cb in zip(self.tagboxes, self.colorboxes): - tags = [t.strip() for t in unicode(tb.currentText()).split(',') if t.strip()] + tags = [t.strip() for t in unicode(tb.text()).split(',') if t.strip()] + tags = '$|^'.join(tags) c = unicode(cb.currentText()).strip() if not tags or not c: continue @@ -114,14 +116,13 @@ class TagWizard(QDialog): _('The color {0} is not valid').format(c), show=True, show_copy_button=False) return False - for t in tags: - lines.append(" in_list(t, ',', '^{0}$', '{1}', '')".format(t, c)) + lines.append(" in_list(t, ',', '^{0}$', '{1}', '')".format(tags, c)) res += ',\n'.join(lines) res += ')\n' self.template = res res = '' for tb, cb in zip(self.tagboxes, self.colorboxes): - t = unicode(tb.currentText()).strip() + t = unicode(tb.text()).strip() if t.endswith(','): t = t[:-1] c = unicode(cb.currentText()).strip() From 2fec4aa6c3719767d52c8e4558697dbc4f088ebb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 24 May 2011 13:33:52 -0600 Subject: [PATCH 05/30] ... --- src/calibre/library/server/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index 862e724809..319feefa44 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -221,7 +221,12 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache, if not ip or ip.startswith('127.'): raise cherrypy.log('Trying to bind to single interface: '+ip) + # Change the host we listen on cherrypy.config.update({'server.socket_host' : ip}) + # This ensures that the change is actually applied + cherrypy.server.socket_host = ip + cherrypy.server.httpserver = cherrypy.server.instance = None + cherrypy.engine.start() self.is_running = True @@ -231,6 +236,8 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache, cherrypy.engine.block() except Exception as e: self.exception = e + import traceback + traceback.print_exc() finally: self.is_running = False try: From 3a78a875afe7c980aae8497d89e1a1d784335c2f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 24 May 2011 15:18:47 -0600 Subject: [PATCH 06/30] Amazon metadata download: Use separate identifiers for country specific downloads so that the links to Amazon in the Book details panel work when downloading metadata from country specific amazon websites. Fixes #786146 (German Amazon Metadata) --- src/calibre/ebooks/metadata/sources/amazon.py | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index f291959475..7da37ce5af 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -29,7 +29,7 @@ class Worker(Thread): # Get details {{{ Get book details from amazons book page in a separate thread ''' - def __init__(self, url, result_queue, browser, log, relevance, plugin, timeout=20): + def __init__(self, url, result_queue, browser, log, relevance, domain, plugin, timeout=20): Thread.__init__(self) self.daemon = True self.url, self.result_queue = url, result_queue @@ -37,7 +37,7 @@ class Worker(Thread): # Get details {{{ self.relevance, self.plugin = relevance, plugin self.browser = browser.clone_browser() self.cover_url = self.amazon_id = self.isbn = None - self.domain = self.plugin.domain + self.domain = domain months = { 'de': { @@ -199,7 +199,8 @@ class Worker(Thread): # Get details {{{ return mi = Metadata(title, authors) - mi.set_identifier('amazon', asin) + idtype = 'amazon' if self.domain == 'com' else 'amazon_'+self.domain + mi.set_identifier(idtype, asin) self.amazon_id = asin try: @@ -404,12 +405,30 @@ class Amazon(Source): 'country\'s Amazon website.'), choices=AMAZON_DOMAINS), ) + def get_domain_and_asin(self, identifiers): + for key, val in identifiers.iteritems(): + key = key.lower() + if key in ('amazon', 'asin'): + return 'com', val + if key.startswith('amazon_'): + domain = key.split('_')[-1] + if domain and domain in self.AMAZON_DOMAINS: + return domain, val + return None, None + def get_book_url(self, identifiers): # {{{ - asin = identifiers.get('amazon', None) - if asin is None: - asin = identifiers.get('asin', None) - if asin: - return ('amazon', asin, 'http://amzn.com/%s'%asin) + domain, asin = self.get_domain_and_asin(identifiers) + if domain and asin: + url = None + if domain == 'com': + url = 'http://amzn.com/'+asin + elif domain == 'uk': + url = 'http://www.amazon.co.uk/dp/'+asin + else: + url = 'http://www.amazon.%s/dp/%s'%(domain, asin) + if url: + idtype = 'amazon' if self.domain == 'com' else 'amazon_'+self.domain + return (idtype, asin, url) # }}} @property @@ -420,8 +439,14 @@ class Amazon(Source): return domain - def create_query(self, log, title=None, authors=None, identifiers={}): # {{{ - domain = self.domain + def create_query(self, log, title=None, authors=None, identifiers={}, # {{{ + domain=None): + if domain is None: + domain = self.domain + + idomain, asin = self.get_domain_and_asin(identifiers) + if idomain is not None: + domain = idomain # See the amazon detailed search page to get all options q = { 'search-alias' : 'aps', @@ -433,7 +458,6 @@ class Amazon(Source): else: q['sort'] = 'relevancerank' - asin = identifiers.get('amazon', None) isbn = check_isbn(identifiers.get('isbn', None)) if asin is not None: @@ -456,23 +480,22 @@ class Amazon(Source): if not ('field-keywords' in q or 'field-isbn' in q or ('field-title' in q)): # Insufficient metadata to make an identify query - return None + return None, None latin1q = dict([(x.encode('latin1', 'ignore'), y.encode('latin1', 'ignore')) for x, y in q.iteritems()]) + udomain = domain if domain == 'uk': - domain = 'co.uk' - url = 'http://www.amazon.%s/s/?'%domain + urlencode(latin1q) - return url + udomain = 'co.uk' + url = 'http://www.amazon.%s/s/?'%udomain + urlencode(latin1q) + return url, domain # }}} def get_cached_cover_url(self, identifiers): # {{{ url = None - asin = identifiers.get('amazon', None) - if asin is None: - asin = identifiers.get('asin', None) + domain, asin = self.get_domain_and_asin(identifiers) if asin is None: isbn = identifiers.get('isbn', None) if isbn is not None: @@ -489,7 +512,7 @@ class Amazon(Source): Note this method will retry without identifiers automatically if no match is found with identifiers. ''' - query = self.create_query(log, title=title, authors=authors, + query, domain = self.create_query(log, title=title, authors=authors, identifiers=identifiers) if query is None: log.error('Insufficient metadata to construct query') @@ -571,7 +594,7 @@ class Amazon(Source): log.error('No matches found with query: %r'%query) return - workers = [Worker(url, result_queue, br, log, i, self) for i, url in + workers = [Worker(url, result_queue, br, log, i, domain, self) for i, url in enumerate(matches)] for w in workers: From 1395c6d5d814349102d23d9cdfb26af8689b4bb0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 24 May 2011 15:48:27 -0600 Subject: [PATCH 07/30] EPUB Output: Change any white-space:pre declarations in the CSS to pre-wrap to accomodate readers that cannot scroll horizontally. Fixes #786722 (chm to epub conversion fails to reflow text in

 tag)

---
 src/calibre/ebooks/epub/output.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py
index 0ed6d7e222..bea90eeba8 100644
--- a/src/calibre/ebooks/epub/output.py
+++ b/src/calibre/ebooks/epub/output.py
@@ -413,6 +413,13 @@ class EPUBOutput(OutputFormatPlugin):
                             rule.style.removeProperty('margin-left')
                             # padding-left breaks rendering in webkit and gecko
                             rule.style.removeProperty('padding-left')
+                # Change whitespace:pre to pre-line to accommodate readers that
+                # cannot scroll horizontally
+                for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
+                    style = rule.style
+                    ws = style.getPropertyValue('white-space')
+                    if ws == 'pre':
+                        style.setProperty('white-space', 'pre-wrap')
 
     # }}}
 

From 50c80a8505c395c43904d0139dbaf908628c7298 Mon Sep 17 00:00:00 2001
From: John Schember 
Date: Tue, 24 May 2011 19:34:04 -0400
Subject: [PATCH 08/30] Store: Fix some bugs. Add basic store chooser which
 give information and allows enabling and disabling store plugins in a user
 friendly manner.

---
 src/calibre/customize/builtins.py             |   4 +-
 src/calibre/gui2/actions/store.py             |   7 +
 src/calibre/gui2/store/config/chooser.py      |  18 +
 .../gui2/store/config/chooser/__init__.py     |   0
 .../config/chooser/adv_search_builder.py      | 131 ++++++
 .../config/chooser/adv_search_builder.ui      | 416 ++++++++++++++++++
 .../store/config/chooser/chooser_dialog.py    |  28 ++
 .../store/config/chooser/chooser_widget.py    |  35 ++
 .../store/config/chooser/chooser_widget.ui    |  87 ++++
 .../gui2/store/config/chooser/models.py       | 244 ++++++++++
 .../gui2/store/config/chooser/results_view.py |  31 ++
 .../gui2/store/config/search/__init__.py      |   0
 .../config/{ => search}/search_widget.py      |   2 +-
 .../config/{ => search}/search_widget.ui      |   0
 src/calibre/gui2/store/config/store.py        |   2 +-
 src/calibre/gui2/store/mobileread/models.py   |   1 +
 .../gui2/store/search/adv_search_builder.py   |   2 +-
 src/calibre/gui2/store/search/search.py       |   2 +-
 src/calibre/gui2/store/search/search.ui       |   7 +-
 19 files changed, 1009 insertions(+), 8 deletions(-)
 create mode 100644 src/calibre/gui2/store/config/chooser.py
 create mode 100644 src/calibre/gui2/store/config/chooser/__init__.py
 create mode 100644 src/calibre/gui2/store/config/chooser/adv_search_builder.py
 create mode 100644 src/calibre/gui2/store/config/chooser/adv_search_builder.ui
 create mode 100644 src/calibre/gui2/store/config/chooser/chooser_dialog.py
 create mode 100644 src/calibre/gui2/store/config/chooser/chooser_widget.py
 create mode 100644 src/calibre/gui2/store/config/chooser/chooser_widget.ui
 create mode 100644 src/calibre/gui2/store/config/chooser/models.py
 create mode 100644 src/calibre/gui2/store/config/chooser/results_view.py
 create mode 100644 src/calibre/gui2/store/config/search/__init__.py
 rename src/calibre/gui2/store/config/{ => search}/search_widget.py (96%)
 rename src/calibre/gui2/store/config/{ => search}/search_widget.ui (100%)

diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index d9e8be00b5..5c90e5699b 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -1316,7 +1316,7 @@ class StoreOpenLibraryStore(StoreBase):
     actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore'
     
     drm_free_only = True
-    headquarters = ['US']
+    headquarters = 'US'
     formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT']
 
 class StoreOReillyStore(StoreBase):
@@ -1381,7 +1381,7 @@ class StoreWoblinkStore(StoreBase):
     actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore'
 
     drm_free_only = False
-    location = 'PL'
+    headquarters = 'PL'
     formats = ['EPUB']
 
 plugins += [
diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py
index c8507e851c..effe470359 100644
--- a/src/calibre/gui2/actions/store.py
+++ b/src/calibre/gui2/actions/store.py
@@ -34,6 +34,8 @@ class StoreAction(InterfaceAction):
         self.store_list_menu = self.store_menu.addMenu(_('Stores'))
         for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()):
             self.store_list_menu.addAction(n, partial(self.open_store, p))
+        self.store_menu.addSeparator()
+        self.store_menu.addAction(_('Choose stores'), self.choose)
         self.qaction.setMenu(self.store_menu)
 
     def do_search(self):
@@ -107,6 +109,11 @@ class StoreAction(InterfaceAction):
         query = 'author:"%s" title:"%s"' % (self._get_author(row), self._get_title(row))
         self.search(query)
 
+    def choose(self):
+        from calibre.gui2.store.config.chooser.chooser_dialog import StoreChooserDialog
+        d = StoreChooserDialog(self.gui)
+        d.exec_()
+
     def open_store(self, store_plugin):
         self.show_disclaimer()
         store_plugin.open(self.gui)
diff --git a/src/calibre/gui2/store/config/chooser.py b/src/calibre/gui2/store/config/chooser.py
new file mode 100644
index 0000000000..f5c40a18ae
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, John Schember '
+__docformat__ = 'restructuredtext en'
+
+'''
+Config widget access functions for enabling and disabling stores.
+'''
+
+def config_widget():
+    from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget
+    return StoreChooserWidget()
+
+def save_settings(config_widget):
+    pass
diff --git a/src/calibre/gui2/store/config/chooser/__init__.py b/src/calibre/gui2/store/config/chooser/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/calibre/gui2/store/config/chooser/adv_search_builder.py b/src/calibre/gui2/store/config/chooser/adv_search_builder.py
new file mode 100644
index 0000000000..7b519abcd1
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser/adv_search_builder.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, John Schember '
+__docformat__ = 'restructuredtext en'
+
+import re
+
+from PyQt4.Qt import (QDialog, QDialogButtonBox)
+
+from calibre.gui2.store.config.chooser.adv_search_builder_ui import Ui_Dialog
+from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
+
+class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
+
+    def __init__(self, parent):
+        QDialog.__init__(self, parent)
+        self.setupUi(self)
+        
+        self.buttonBox.accepted.connect(self.advanced_search_button_pushed)
+        self.tab_2_button_box.accepted.connect(self.accept)
+        self.tab_2_button_box.rejected.connect(self.reject)
+        self.clear_button.clicked.connect(self.clear_button_pushed)
+        self.adv_search_used = False
+        self.mc = ''
+        
+        self.tabWidget.setCurrentIndex(0)
+        self.tabWidget.currentChanged[int].connect(self.tab_changed)
+        self.tab_changed(0)
+        
+    def tab_changed(self, idx):
+        if idx == 1:
+            self.tab_2_button_box.button(QDialogButtonBox.Ok).setDefault(True)
+        else:
+            self.buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
+
+    def advanced_search_button_pushed(self):
+        self.adv_search_used = True
+        self.accept()
+
+    def clear_button_pushed(self):
+        self.name_box.setText('')
+        self.description_box.setText('')
+        self.headquarters_box.setText('')
+        self.format_box.setText('')
+        self.enabled_combo.setIndex(0)
+        self.drm_combo.setIndex(0)
+
+    def tokens(self, raw):
+        phrases = re.findall(r'\s*".*?"\s*', raw)
+        for f in phrases:
+            raw = raw.replace(f, ' ')
+        phrases = [t.strip('" ') for t in phrases]
+        return ['"' + self.mc + t + '"' for t in phrases + [r.strip() for r in raw.split()]]
+
+    def search_string(self):
+        if self.adv_search_used:
+            return self.adv_search_string()
+        else:
+            return self.box_search_string()
+
+    def adv_search_string(self):
+        mk = self.matchkind.currentIndex()
+        if mk == CONTAINS_MATCH:
+            self.mc = ''
+        elif mk == EQUALS_MATCH:
+            self.mc = '='
+        else:
+            self.mc = '~'
+        all, any, phrase, none = map(lambda x: unicode(x.text()),
+                (self.all, self.any, self.phrase, self.none))
+        all, any, none = map(self.tokens, (all, any, none))
+        phrase = phrase.strip()
+        all = ' and '.join(all)
+        any = ' or '.join(any)
+        none = ' and not '.join(none)
+        ans = ''
+        if phrase:
+            ans += '"%s"'%phrase
+        if all:
+            ans += (' and ' if ans else '') + all
+        if none:
+            ans += (' and not ' if ans else 'not ') + none
+        if any:
+            ans += (' or ' if ans else '') + any
+        return ans
+
+    def token(self):
+        txt = unicode(self.text.text()).strip()
+        if txt:
+            if self.negate.isChecked():
+                txt = '!'+txt
+            tok = self.FIELDS[unicode(self.field.currentText())]+txt
+            if re.search(r'\s', tok):
+                tok = '"%s"'%tok
+            return tok
+
+    def box_search_string(self):
+        mk = self.matchkind.currentIndex()
+        if mk == CONTAINS_MATCH:
+            self.mc = ''
+        elif mk == EQUALS_MATCH:
+            self.mc = '='
+        else:
+            self.mc = '~'
+
+        ans = []
+        self.box_last_values = {}
+        name = unicode(self.name_box.text()).strip()
+        if name:
+            ans.append('name:"' + self.mc + name + '"')
+        description = unicode(self.description_box.text()).strip()
+        if description:
+            ans.append('description:"' + self.mc + description + '"')
+        headquarters = unicode(self.headquarters_box.text()).strip()
+        if headquarters:
+            ans.append('headquarters:"' + self.mc + headquarters + '"')
+        format = unicode(self.format_box.text()).strip()
+        if format:
+            ans.append('format:"' + self.mc + format + '"')
+        enabled = unicode(self.enabled_combo.currentText()).strip()
+        if enabled:
+            ans.append('enabled:' + enabled)
+        drm = unicode(self.drm_combo.currentText()).strip()
+        if drm:
+            ans.append('drm:' + drm)
+        if ans:
+            return ' and '.join(ans)
+        return ''
diff --git a/src/calibre/gui2/store/config/chooser/adv_search_builder.ui b/src/calibre/gui2/store/config/chooser/adv_search_builder.ui
new file mode 100644
index 0000000000..7d57321c72
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser/adv_search_builder.ui
@@ -0,0 +1,416 @@
+
+
+ Dialog
+ 
+  
+   
+    0
+    0
+    752
+    472
+   
+  
+  
+   Advanced Search
+  
+  
+   
+    :/images/search.png:/images/search.png
+  
+  
+   
+    
+     
+      &What kind of match to use:
+     
+     
+      matchkind
+     
+    
+   
+   
+    
+     
+      
+       Contains: the word or phrase matches anywhere in the metadata field
+      
+     
+     
+      
+       Equals: the word or phrase must match the entire metadata field
+      
+     
+     
+      
+       Regular expression: the expression must match anywhere in the metadata field
+      
+     
+    
+   
+   
+    
+     
+      0
+     
+     
+      
+       A&dvanced Search
+      
+      
+       
+        
+         
+          Find entries that have...
+         
+         
+          
+           
+            
+             
+              
+               &All these words:
+              
+              
+               all
+              
+             
+            
+            
+             
+            
+           
+          
+          
+           
+            
+             
+              
+               This exact &phrase:
+              
+              
+               all
+              
+             
+            
+            
+             
+            
+           
+          
+          
+           
+            
+             
+              
+               &One or more of these words:
+              
+              
+               all
+              
+             
+            
+            
+             
+            
+           
+          
+         
+        
+       
+       
+        
+         
+          But dont show entries that have...
+         
+         
+          
+           
+            
+             
+              
+               Any of these &unwanted words:
+              
+              
+               all
+              
+             
+            
+            
+             
+            
+           
+          
+          
+           
+            
+             
+              16777215
+              30
+             
+            
+            
+             See the <a href="http://calibre-ebook.com/user_manual/gui.html#the-search-interface">User Manual</a> for more help
+            
+            
+             true
+            
+           
+          
+         
+        
+       
+       
+        
+         
+          Qt::Vertical
+         
+         
+          
+           20
+           40
+          
+         
+        
+       
+       
+        
+         
+          Qt::Horizontal
+         
+         
+          QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+         
+        
+       
+      
+     
+     
+      
+       Nam&e/Description ...
+      
+      
+       
+        
+         
+          &Name:
+         
+         
+          name_box
+         
+        
+       
+       
+        
+         
+          Enter the title.
+         
+        
+       
+       
+        
+         
+          &Description:
+         
+         
+          description_box
+         
+        
+       
+       
+        
+         
+          &Headquarters:
+         
+         
+          headquarters_box
+         
+        
+       
+       
+        
+         
+          
+           
+            &Clear
+           
+          
+         
+         
+          
+           
+            QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+           
+          
+         
+        
+       
+       
+        
+         
+          Qt::Vertical
+         
+         
+          
+           20
+           40
+          
+         
+        
+       
+       
+        
+         
+          Search only in specific fields:
+         
+        
+       
+       
+        
+       
+       
+        
+       
+       
+        
+         
+          &Format:
+         
+         
+          format_box
+         
+        
+       
+       
+        
+       
+       
+        
+         
+          Enabled:
+         
+        
+       
+       
+        
+         
+          DRM:
+         
+        
+       
+       
+        
+         
+          
+           
+          
+         
+         
+          
+           true
+          
+         
+         
+          
+           false
+          
+         
+        
+       
+       
+        
+         
+          
+           
+          
+         
+         
+          
+           true
+          
+         
+         
+          
+           false
+          
+         
+        
+       
+      
+     
+    
+   
+   
+    
+     
+      Qt::Vertical
+     
+     
+      
+       20
+       40
+      
+     
+    
+   
+  
+ 
+ 
+  
+   EnLineEdit
+   QLineEdit
+   
widgets.h
+
+
+ + all + phrase + any + none + buttonBox + name_box + description_box + headquarters_box + format_box + clear_button + tab_2_button_box + tabWidget + matchkind + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/calibre/gui2/store/config/chooser/chooser_dialog.py b/src/calibre/gui2/store/config/chooser/chooser_dialog.py new file mode 100644 index 0000000000..c94796dc11 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/chooser_dialog.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import (QDialog, QDialogButtonBox, QVBoxLayout) + +from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget + +class StoreChooserDialog(QDialog): + + def __init__(self, parent): + QDialog.__init__(self, parent) + + self.setWindowTitle(_('Choose stores')) + + button_box = QDialogButtonBox(QDialogButtonBox.Close) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + v = QVBoxLayout(self) + self.config_widget = StoreChooserWidget() + v.addWidget(self.config_widget) + v.addWidget(button_box) + + self.resize(800, 600) diff --git a/src/calibre/gui2/store/config/chooser/chooser_widget.py b/src/calibre/gui2/store/config/chooser/chooser_widget.py new file mode 100644 index 0000000000..93630d69a7 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/chooser_widget.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import (QWidget, QIcon, QDialog) + +from calibre.gui2.store.config.chooser.adv_search_builder import AdvSearchBuilderDialog +from calibre.gui2.store.config.chooser.chooser_widget_ui import Ui_Form + +class StoreChooserWidget(QWidget, Ui_Form): + + def __init__(self): + QWidget.__init__(self) + self.setupUi(self) + + self.adv_search_builder.setIcon(QIcon(I('search.png'))) + + self.search.clicked.connect(self.do_search) + self.adv_search_builder.clicked.connect(self.build_adv_search) + self.results_view.activated.connect(self.toggle_plugin) + + def do_search(self): + self.results_view.model().search(unicode(self.query.text())) + + def toggle_plugin(self, index): + self.results_view.model().toggle_plugin(index) + + def build_adv_search(self): + adv = AdvSearchBuilderDialog(self) + if adv.exec_() == QDialog.Accepted: + self.query.setText(adv.search_string()) diff --git a/src/calibre/gui2/store/config/chooser/chooser_widget.ui b/src/calibre/gui2/store/config/chooser/chooser_widget.ui new file mode 100644 index 0000000000..69117406b1 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/chooser_widget.ui @@ -0,0 +1,87 @@ + + + Form + + + + 0 + 0 + 610 + 553 + + + + Form + + + + + + + + Query: + + + + + + + ... + + + + + + + + + + Search + + + + + + + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + true + + + false + + + true + + + false + + + false + + + + + + + + ResultsView + QTreeView +
results_view.h
+
+
+ + +
diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py new file mode 100644 index 0000000000..460b698878 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/models.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import (Qt, QAbstractItemModel, QIcon, QVariant, QModelIndex) + +from calibre.gui2 import NONE +from calibre.customize.ui import is_disabled, disable_plugin, enable_plugin +from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ + REGEXP_MATCH +from calibre.utils.icu import sort_key +from calibre.utils.search_query_parser import SearchQueryParser + + +class Matches(QAbstractItemModel): + + HEADERS = [_('Enabled'), _('Name'), _('No DRM'), _('Headquarters'), _('Formats')] + HTML_COLS = [1] + + def __init__(self, plugins): + QAbstractItemModel.__init__(self) + + self.NO_DRM_ICON = QIcon(I('ok.png')) + + self.all_matches = plugins + self.matches = plugins + self.filter = '' + self.search_filter = SearchFilter(self.all_matches) + + self.sort_col = 1 + self.sort_order = Qt.AscendingOrder + + def get_plugin(self, index): + row = index.row() + if row < len(self.matches): + return self.matches[row] + else: + return None + + def search(self, filter): + self.filter = filter.strip() + if not self.filter: + self.matches = self.all_matches + else: + try: + self.matches = list(self.search_filter.parse(self.filter)) + except: + self.matches = self.all_matches + self.layoutChanged.emit() + self.sort(self.sort_col, self.sort_order) + + def toggle_plugin(self, index): + new_index = self.createIndex(index.row(), 0) + data = QVariant(is_disabled(self.get_plugin(index))) + self.setData(new_index, data, Qt.CheckStateRole) + + def index(self, row, column, parent=QModelIndex()): + return self.createIndex(row, column) + + def parent(self, index): + if not index.isValid() or index.internalId() == 0: + return QModelIndex() + return self.createIndex(0, 0) + + def rowCount(self, *args): + return len(self.matches) + + def columnCount(self, *args): + return len(self.HEADERS) + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return NONE + text = '' + if orientation == Qt.Horizontal: + if section < len(self.HEADERS): + text = self.HEADERS[section] + return QVariant(text) + else: + return QVariant(section+1) + + def data(self, index, role): + row, col = index.row(), index.column() + result = self.matches[row] + if role in (Qt.DisplayRole, Qt.EditRole): + if col == 1: + return QVariant('%s
%s' % (result.name, result.description)) + elif col == 3: + return QVariant(result.headquarters) + elif col == 4: + return QVariant(', '.join(result.formats).upper()) + elif role == Qt.DecorationRole: + if col == 2: + if result.drm_free_only: + return QVariant(self.NO_DRM_ICON) + elif role == Qt.CheckStateRole: + if col == 0: + if is_disabled(result): + return Qt.Unchecked + return Qt.Checked + elif role == Qt.ToolTipRole: + return QVariant('

%s

' % result.description) + return NONE + + def setData(self, index, data, role): + if not index.isValid(): + return False + row, col = index.row(), index.column() + if col == 0: + if data.toBool(): + enable_plugin(self.get_plugin(index)) + else: + disable_plugin(self.get_plugin(index)) + self.dataChanged.emit(self.index(index.row(), 0), self.index(index.row(), self.columnCount() - 1)) + return True + + def flags(self, index): + if index.column() == 0: + return QAbstractItemModel.flags(self, index) | Qt.ItemIsUserCheckable + return QAbstractItemModel.flags(self, index) + + def data_as_text(self, match, col): + text = '' + if col == 0: + text = 'b' if is_disabled(match) else 'a' + elif col == 1: + text = match.name + elif col == 2: + text = 'b' if match.drm else 'a' + elif col == 3: + text = match.headquarteres + return text + + def sort(self, col, order, reset=True): + self.sort_col = col + self.sort_order = order + if not self.matches: + return + descending = order == Qt.DescendingOrder + self.matches.sort(None, + lambda x: sort_key(unicode(self.data_as_text(x, col))), + descending) + if reset: + self.reset() + + +class SearchFilter(SearchQueryParser): + + USABLE_LOCATIONS = [ + 'all', + 'description', + 'drm', + 'enabled', + 'format', + 'formats', + 'headquarters', + 'name', + ] + + def __init__(self, all_plugins=[]): + SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS) + self.srs = set(all_plugins) + + def universal_set(self): + return self.srs + + def get_matches(self, location, query): + location = location.lower().strip() + if location == 'formats': + location = 'format' + + matchkind = CONTAINS_MATCH + if len(query) > 1: + if query.startswith('\\'): + query = query[1:] + elif query.startswith('='): + matchkind = EQUALS_MATCH + query = query[1:] + elif query.startswith('~'): + matchkind = REGEXP_MATCH + query = query[1:] + if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D + query = query.lower() + + if location not in self.USABLE_LOCATIONS: + return set([]) + matches = set([]) + all_locs = set(self.USABLE_LOCATIONS) - set(['all']) + locations = all_locs if location == 'all' else [location] + q = { + 'description': lambda x: x.description.lower(), + 'drm': lambda x: not x.drm_free_only, + 'enabled': lambda x: not is_disabled(x), + 'format': lambda x: ','.join(x.formats).lower(), + 'headquarters': lambda x: x.headquarters.lower(), + 'name': lambda x : x.name.lower(), + } + q['formats'] = q['format'] + for sr in self.srs: + for locvalue in locations: + accessor = q[locvalue] + if query == 'true': + if locvalue in ('drm', 'enabled'): + if accessor(sr) == True: + matches.add(sr) + elif accessor(sr) is not None: + matches.add(sr) + continue + if query == 'false': + if locvalue in ('drm', 'enabled'): + if accessor(sr) == False: + matches.add(sr) + elif accessor(sr) is None: + matches.add(sr) + continue + # this is bool, so can't match below + if locvalue in ('drm', 'enabled'): + continue + try: + ### Can't separate authors because comma is used for name sep and author sep + ### Exact match might not get what you want. For that reason, turn author + ### exactmatch searches into contains searches. + if locvalue == 'name' and matchkind == EQUALS_MATCH: + m = CONTAINS_MATCH + else: + m = matchkind + + if locvalue == 'format': + vals = accessor(sr).split(',') + else: + vals = [accessor(sr)] + if _match(query, vals, m): + matches.add(sr) + break + except ValueError: # Unicode errors + import traceback + traceback.print_exc() + return matches + + \ No newline at end of file diff --git a/src/calibre/gui2/store/config/chooser/results_view.py b/src/calibre/gui2/store/config/chooser/results_view.py new file mode 100644 index 0000000000..52d7696e4f --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/results_view.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import (QTreeView, QSize) + +from calibre.customize.ui import store_plugins +from calibre.gui2.metadata.single_download import RichTextDelegate +from calibre.gui2.store.config.chooser.models import Matches + +class ResultsView(QTreeView): + + def __init__(self, *args): + QTreeView.__init__(self,*args) + + self._model = Matches([p for p in store_plugins()]) + self.setModel(self._model) + + self.setIconSize(QSize(24, 24)) + + self.rt_delegate = RichTextDelegate(self) + + for i in self._model.HTML_COLS: + self.setItemDelegateForColumn(i, self.rt_delegate) + + for i in xrange(self._model.columnCount()): + self.resizeColumnToContents(i) diff --git a/src/calibre/gui2/store/config/search/__init__.py b/src/calibre/gui2/store/config/search/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/gui2/store/config/search_widget.py b/src/calibre/gui2/store/config/search/search_widget.py similarity index 96% rename from src/calibre/gui2/store/config/search_widget.py rename to src/calibre/gui2/store/config/search/search_widget.py index 43e911a432..b2e55d2ad1 100644 --- a/src/calibre/gui2/store/config/search_widget.py +++ b/src/calibre/gui2/store/config/search/search_widget.py @@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import QWidget from calibre.gui2 import JSONConfig -from calibre.gui2.store.config.search_widget_ui import Ui_Form +from calibre.gui2.store.config.search.search_widget_ui import Ui_Form class StoreConfigWidget(QWidget, Ui_Form): diff --git a/src/calibre/gui2/store/config/search_widget.ui b/src/calibre/gui2/store/config/search/search_widget.ui similarity index 100% rename from src/calibre/gui2/store/config/search_widget.ui rename to src/calibre/gui2/store/config/search/search_widget.ui diff --git a/src/calibre/gui2/store/config/store.py b/src/calibre/gui2/store/config/store.py index ddc24870bd..852f602d08 100644 --- a/src/calibre/gui2/store/config/store.py +++ b/src/calibre/gui2/store/config/store.py @@ -11,7 +11,7 @@ Config widget access functions for configuring the store action. ''' def config_widget(): - from calibre.gui2.store.config.search_widget import StoreConfigWidget + from calibre.gui2.store.config.search.search_widget import StoreConfigWidget return StoreConfigWidget() def save_settings(config_widget): diff --git a/src/calibre/gui2/store/mobileread/models.py b/src/calibre/gui2/store/mobileread/models.py index a080affb51..297707e248 100644 --- a/src/calibre/gui2/store/mobileread/models.py +++ b/src/calibre/gui2/store/mobileread/models.py @@ -47,6 +47,7 @@ class BooksModel(QAbstractItemModel): self.books = list(self.search_filter.parse(self.filter)) except: self.books = self.all_books + self.layoutChanged.emit() self.sort(self.sort_col, self.sort_order) self.total_changed.emit(self.rowCount()) diff --git a/src/calibre/gui2/store/search/adv_search_builder.py b/src/calibre/gui2/store/search/adv_search_builder.py index 50d4d3f3f4..745e709f90 100644 --- a/src/calibre/gui2/store/search/adv_search_builder.py +++ b/src/calibre/gui2/store/search/adv_search_builder.py @@ -116,7 +116,7 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog): if price: ans.append('price:"' + self.mc + price + '"') format = unicode(self.format_box.text()).strip() - if author: + if format: ans.append('format:"' + self.mc + format + '"') if ans: return ' and '.join(ans) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index c7c252034d..ffc6ec097e 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -14,7 +14,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator -from calibre.gui2.store.config.search_widget import StoreConfigWidget +from calibre.gui2.store.config.search.search_widget import StoreConfigWidget from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.search.download_thread import SearchThreadPool, \ CacheUpdateThreadPool diff --git a/src/calibre/gui2/store/search/search.ui b/src/calibre/gui2/store/search/search.ui index 0360fa5f98..1451aa09f1 100644 --- a/src/calibre/gui2/store/search/search.ui +++ b/src/calibre/gui2/store/search/search.ui @@ -82,8 +82,8 @@ 0 0 - 102 - 129 + 125 + 127 @@ -159,6 +159,9 @@ false + + false + From b871315864d5e39a9f9e768b37d5268387decf73 Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 24 May 2011 19:45:20 -0400 Subject: [PATCH 09/30] Store: Chooser, set sort and sort indicator. --- src/calibre/gui2/store/config/chooser/results_view.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/config/chooser/results_view.py b/src/calibre/gui2/store/config/chooser/results_view.py index 52d7696e4f..1c18a18d7b 100644 --- a/src/calibre/gui2/store/config/chooser/results_view.py +++ b/src/calibre/gui2/store/config/chooser/results_view.py @@ -6,7 +6,7 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QTreeView, QSize) +from PyQt4.Qt import (Qt, QTreeView, QSize) from calibre.customize.ui import store_plugins from calibre.gui2.metadata.single_download import RichTextDelegate @@ -29,3 +29,6 @@ class ResultsView(QTreeView): for i in xrange(self._model.columnCount()): self.resizeColumnToContents(i) + + self.model().sort(1, Qt.AscendingOrder) + self.header().setSortIndicator(self.model().sort_col, self.model().sort_order) From 41cc4be9528bfd8835e1f0b67bc6aff494d579f1 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 25 May 2011 06:55:52 -0400 Subject: [PATCH 10/30] Store: Reload store plugins accessible by GUI after using the store chooser. --- src/calibre/gui2/actions/store.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py index effe470359..0fd783f0a3 100644 --- a/src/calibre/gui2/actions/store.py +++ b/src/calibre/gui2/actions/store.py @@ -113,6 +113,8 @@ class StoreAction(InterfaceAction): from calibre.gui2.store.config.chooser.chooser_dialog import StoreChooserDialog d = StoreChooserDialog(self.gui) d.exec_() + self.gui.load_store_plugins() + self.load_menu() def open_store(self, store_plugin): self.show_disclaimer() From a8deb0ed7d2b349a82711f96cb0ed28a3db16156 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 25 May 2011 09:56:27 -0600 Subject: [PATCH 11/30] ... --- src/calibre/devices/android/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index db473a755e..1cdf394c24 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -59,7 +59,7 @@ class ANDROID(USBMS): 0x0489 : { 0xc001 : [0x0226], 0xc004 : [0x0226], }, # Acer - 0x502 : { 0x3203 : [0x0100]}, + 0x502 : { 0x3203 : [0x0100, 0x224]}, # Dell 0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]}, From ce6cedf730c2c59b0cc41bcce7c29a969edf2eaf Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Thu, 26 May 2011 00:24:04 +0800 Subject: [PATCH 12/30] [Bug] Error in passing API key to douban API. --- src/calibre/ebooks/metadata/sources/douban.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/sources/douban.py b/src/calibre/ebooks/metadata/sources/douban.py index 3c6bb7b6c7..8a95c4ed6b 100644 --- a/src/calibre/ebooks/metadata/sources/douban.py +++ b/src/calibre/ebooks/metadata/sources/douban.py @@ -211,7 +211,7 @@ class Douban(Source): 'q': q, }) if self.DOUBAN_API_KEY and self.DOUBAN_API_KEY != '': - url = url + "?apikey=" + self.DOUBAN_API_KEY + url = url + "&apikey=" + self.DOUBAN_API_KEY return url # }}} From 70d1f3c046913e90cfd649859b2a2d32528a88b9 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Thu, 26 May 2011 00:31:47 +0800 Subject: [PATCH 13/30] [Bug] Error in passing API key to douban API when search by isbn or douban id --- src/calibre/ebooks/metadata/sources/douban.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/sources/douban.py b/src/calibre/ebooks/metadata/sources/douban.py index 8a95c4ed6b..70bf01a00e 100644 --- a/src/calibre/ebooks/metadata/sources/douban.py +++ b/src/calibre/ebooks/metadata/sources/douban.py @@ -211,7 +211,10 @@ class Douban(Source): 'q': q, }) if self.DOUBAN_API_KEY and self.DOUBAN_API_KEY != '': - url = url + "&apikey=" + self.DOUBAN_API_KEY + if t == "isbn" or t == "subject": + url = url + "?apikey=" + self.DOUBAN_API_KEY + else: + url = url + "&apikey=" + self.DOUBAN_API_KEY return url # }}} From b707ae28202b550a704c0729b040e254fcaca51e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 May 2011 17:44:47 +0100 Subject: [PATCH 14/30] Addition of boolean template functions and, or, not. Change documentation to include them. Add a function classification summary to the documentation. --- src/calibre/manual/template_lang.rst | 20 +++++++++- src/calibre/utils/formatter_functions.py | 51 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index 69c77e5bfd..16a90f7531 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -229,13 +229,14 @@ For various values of series_index, the program returns: The following functions are available in addition to those described in single-function mode. Remember from the example above that the single-function mode functions require an additional first parameter specifying the field to operate on. With the exception of the ``id`` parameter of assign, all parameters can be statements (sequences of expressions): + * ``and(value, value, ...)`` -- returns the string "1" if all values are not empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want. * ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers. * ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression * ``booksize()`` -- returns the value of the |app| 'size' field. Returns '' if there are no formats. * ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers. * ``field(name)`` -- returns the metadata field named by ``name``. - * ``first_non_empty(value, value, ...) -- returns the first value that is not empty. If all values are empty, then the empty value is returned. You can have as many values as you want. + * ``first_non_empty(value, value, ...)`` -- returns the first value that is not empty. If all values are empty, then the empty value is returned. You can have as many values as you want. * ``format_date(x, date_format)`` -- format_date(val, format_string) -- format the value, which must be a date field, using the format_string, returning a string. The formatting codes are:: d : the day as number without a leading zero (1 to 31) @@ -251,7 +252,9 @@ The following functions are available in addition to those described in single-f iso : the date with time and timezone. Must be the only format present. * ``eval(string)`` -- evaluates the string as a program, passing the local variables (those ``assign`` ed to). This permits using the template processor to construct complex results from local variables. + * ``not(value)`` -- returns the string "1" if the value is empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want. * ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers. + * ``or(value, value, ...)`` -- returns the string "1" if any value is not empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want. * ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole. * ``raw_field(name)`` -- returns the metadata field named by name without applying any formatting. * ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments. @@ -259,7 +262,22 @@ The following functions are available in addition to those described in single-f * ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``. * ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers. * ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value. + +Function classification summary: + * Get values from metadata: ``field``. ``raw_field``. In some situations, ``lookup`` can be used in place of ``field``. + * Arithmetic: ``add``, ``subtract``, ``multiply``, ``divide`` + * Boolean: ``and``, ``or``, ``not``. The function ``if_empty`` is similar to ``and`` called with one argument. + * If-then-else: ``contains``, ``test`` + * Iterating over values: ``first_non_empty``, ``lookup``, ``switch`` + * List lookup: ``in_list``, ``list_item``, ``select``, + * List manipulation: ``count``, ``sublist``, ``subitems`` + * Recursion: ``eval``, ``template`` + * Relational: ``cmp`` , ``strcmp`` for strings + * String case changes: ``lowercase``, ``uppercase``, ``titlecase``, ``capitalize`` + * String manipulation: ``re``, ``shorten``, ``substr`` + * Other: ``assign``, ``booksize``, ``print``, ``format_date``, + .. _general_mode: Using general program mode diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index c53277f3ce..a3a156648f 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -594,7 +594,56 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction): i += 1 return '' +class BuiltinAnd(BuiltinFormatterFunction): + name = 'and' + arg_count = -1 + doc = _('and(value, value, ...) -- ' + 'returns the string "1" if all values are not empty, otherwise ' + 'returns the empty string. This function works well with test or ' + 'first_non_empty. You can have as many values as you want.') + + def evaluate(self, formatter, kwargs, mi, locals, *args): + i = 0 + while i < len(args): + if not args[i]: + return '' + i += 1 + return '1' + +class BuiltinOr(BuiltinFormatterFunction): + name = 'or' + arg_count = -1 + doc = _('or(value, value, ...) -- ' + 'returns the string "1" if any value is not empty, otherwise ' + 'returns the empty string. This function works well with test or ' + 'first_non_empty. You can have as many values as you want.') + + def evaluate(self, formatter, kwargs, mi, locals, *args): + i = 0 + while i < len(args): + if args[i]: + return '1' + i += 1 + return '' + +class BuiltinNot(BuiltinFormatterFunction): + name = 'not' + arg_count = 1 + doc = _('not(value) -- ' + 'returns the string "1" if the value is empty, otherwise ' + 'returns the empty string. This function works well with test or ' + 'first_non_empty. You can have as many values as you want.') + + def evaluate(self, formatter, kwargs, mi, locals, *args): + i = 0 + while i < len(args): + if args[i]: + return '1' + i += 1 + return '' + builtin_add = BuiltinAdd() +builtin_and = BuiltinAnd() builtin_assign = BuiltinAssign() builtin_booksize = BuiltinBooksize() builtin_capitalize = BuiltinCapitalize() @@ -612,6 +661,8 @@ builtin_list_item = BuiltinListitem() builtin_lookup = BuiltinLookup() builtin_lowercase = BuiltinLowercase() builtin_multiply = BuiltinMultiply() +builtin_not = BuiltinNot() +builtin_or = BuiltinOr() builtin_print = BuiltinPrint() builtin_raw_field = BuiltinRaw_field() builtin_re = BuiltinRe() From aa30d964db0b8fbacfb963cfce3fd08cb30759cd Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 May 2011 18:09:41 +0100 Subject: [PATCH 15/30] Slight improvement to user device faq entry --- src/calibre/manual/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index d3784eda6f..b120fd4a1b 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -138,7 +138,7 @@ Follow these steps to find the problem: My device is non-standard or unusual. What can I do to connect to it? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that presents that shows up as a disk drive in your operating system. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information. +In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that shows up as a disk drive in your operating system. Note: on windows, the device must have a drive letter for calibre to use it. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information. How does |app| manage collections on my SONY reader? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From e1cf8e63602d9f412997d57e658b3c30a70b0546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Wed, 25 May 2011 21:33:30 +0200 Subject: [PATCH 16/30] Virtualo Store --- src/calibre/customize/builtins.py | 11 ++++ src/calibre/gui2/store/virtualo_plugin.py | 72 +++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/calibre/gui2/store/virtualo_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index d9e8be00b5..ac5377700e 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1346,6 +1346,16 @@ class StoreSmashwordsStore(StoreBase): headquarters = 'US' formats = ['EPUB', 'HTML', 'LRF', 'MOBI', 'PDB', 'RTF', 'TXT'] +class StoreVirtualoStore(StoreBase): + name = 'Virtualo' + author = 'Tomasz Długosz' + description = u'Księgarnia internetowa, która oferuje bezpieczny i szeroki dostęp do książek w formie cyfrowej.' + actual_plugin = 'calibre.gui2.store.virtualo_plugin:VirtualoStore' + + drm_free_only = False + location = 'PL' + formats = ['EPUB', 'PDF'] + class StoreWaterstonesUKStore(StoreBase): name = 'Waterstones UK' author = 'Charles Haley' @@ -1411,6 +1421,7 @@ plugins += [ StoreOReillyStore, StorePragmaticBookshelfStore, StoreSmashwordsStore, + StoreVirtualoStore, StoreWaterstonesUKStore, StoreWeightlessBooksStore, StoreWizardsTowerBooksStore, diff --git a/src/calibre/gui2/store/virtualo_plugin.py b/src/calibre/gui2/store/virtualo_plugin.py new file mode 100644 index 0000000000..b1fe0091b6 --- /dev/null +++ b/src/calibre/gui2/store/virtualo_plugin.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, Tomasz Długosz ' +__docformat__ = 'restructuredtext en' + +import re +import urllib +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class VirtualoStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + url = 'http://virtualo.pl/ebook/c2/' + detail_url = None + + if external or self.config.get('open_external', False): + open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(self.config.get('tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://virtualo.pl/c2/?q=' + urllib.quote(query.encode('utf-8')) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@id="product_list"]/div/div[@class="column"]'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//table/tr[2]/td[1]/a/@href')) + if not id: + continue + + price = ''.join(data.xpath('.//span[@class="price"]/text() | .//span[@class="price abbr"]/text()')) + cover_url = ''.join(data.xpath('.//table/tr[2]/td[1]/a/img/@src')) + title = ''.join(data.xpath('.//div[@class="title"]/a/text()')) + author = ', '.join(data.xpath('.//div[@class="authors"]/a/text()')) + formats = ', '.join(data.xpath('.//span[@class="format"]/a/text()')) + formats = re.sub(r'(, )?ONLINE(, )?', '', formats) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + ' ' + formats + s.author = author.strip() + s.price = price + ' zł' + s.detail_item = 'http://virtualo.pl' + id.strip() + s.formats = formats.upper().strip() + s.drm = SearchResult.DRM_LOCKED if formats == 'EPUB' else SearchResult.DRM_UNKNOWN + + yield s From df81def0cf452d5063567e5aae8cdccf5f2e1c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Wed, 25 May 2011 21:44:33 +0200 Subject: [PATCH 17/30] fix Virtualo headquarters --- src/calibre/customize/builtins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 5a56a89cf0..14565da152 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1353,7 +1353,7 @@ class StoreVirtualoStore(StoreBase): actual_plugin = 'calibre.gui2.store.virtualo_plugin:VirtualoStore' drm_free_only = False - location = 'PL' + headquarters = 'PL' formats = ['EPUB', 'PDF'] class StoreWaterstonesUKStore(StoreBase): From 75782ed4f2c0dec3f27a440f831f455bee8400dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Wed, 25 May 2011 21:56:08 +0200 Subject: [PATCH 18/30] fix headquarters in chooser --- src/calibre/gui2/store/config/chooser/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py index 460b698878..f76ca45a01 100644 --- a/src/calibre/gui2/store/config/chooser/models.py +++ b/src/calibre/gui2/store/config/chooser/models.py @@ -132,7 +132,7 @@ class Matches(QAbstractItemModel): elif col == 2: text = 'b' if match.drm else 'a' elif col == 3: - text = match.headquarteres + text = match.headquarters return text def sort(self, col, order, reset=True): @@ -241,4 +241,4 @@ class SearchFilter(SearchQueryParser): traceback.print_exc() return matches - \ No newline at end of file + From 3734b12cc5a0d36d528bc2c4281d87c04bd1d231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Wed, 25 May 2011 22:05:06 +0200 Subject: [PATCH 19/30] not all EPUBs have DRM in Virtualo --- src/calibre/gui2/store/virtualo_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/virtualo_plugin.py b/src/calibre/gui2/store/virtualo_plugin.py index b1fe0091b6..d86aa3e0e5 100644 --- a/src/calibre/gui2/store/virtualo_plugin.py +++ b/src/calibre/gui2/store/virtualo_plugin.py @@ -67,6 +67,6 @@ class VirtualoStore(BasicStoreConfig, StorePlugin): s.price = price + ' zł' s.detail_item = 'http://virtualo.pl' + id.strip() s.formats = formats.upper().strip() - s.drm = SearchResult.DRM_LOCKED if formats == 'EPUB' else SearchResult.DRM_UNKNOWN + s.drm = SearchResult.DRM_UNKNOWN yield s From 7b98249b90366e26900d2b2cc5a86ff54595b083 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 25 May 2011 17:51:29 -0400 Subject: [PATCH 20/30] Store: chooser, fix sorting by drm. --- src/calibre/gui2/store/config/chooser/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py index 0c784f6614..3ceed6fb00 100644 --- a/src/calibre/gui2/store/config/chooser/models.py +++ b/src/calibre/gui2/store/config/chooser/models.py @@ -130,7 +130,7 @@ class Matches(QAbstractItemModel): elif col == 1: text = match.name elif col == 2: - text = 'b' if getattr(match, 'drm', True) else 'a' + text = 'a' if getattr(match, 'drm_free_only', True) else 'b' elif col == 3: text = getattr(match, 'headquarters', '') return text From 47cd16b6656e3f830f67fabc36fd3169242293c3 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 25 May 2011 18:53:11 -0400 Subject: [PATCH 21/30] Store: ... --- src/calibre/customize/builtins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 14565da152..bbbb8a738f 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1348,7 +1348,7 @@ class StoreSmashwordsStore(StoreBase): class StoreVirtualoStore(StoreBase): name = 'Virtualo' - author = 'Tomasz Długosz' + author = u'Tomasz Długosz' description = u'Księgarnia internetowa, która oferuje bezpieczny i szeroki dostęp do książek w formie cyfrowej.' actual_plugin = 'calibre.gui2.store.virtualo_plugin:VirtualoStore' @@ -1386,7 +1386,7 @@ class StoreWizardsTowerBooksStore(StoreBase): class StoreWoblinkStore(StoreBase): name = 'Woblink' - author = 'Tomasz Długosz' + author = u'Tomasz Długosz' description = u'Czytanie zdarza się wszędzie!' actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore' From c6e98f3af75ed803b50a83a4125bd014075a6ef1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 25 May 2011 19:02:53 -0600 Subject: [PATCH 22/30] Dont allow user to use non email usernames when setting up Hotmail or Gmail accounts --- src/calibre/gui2/wizard/send_email.py | 108 +++++++++++++++----------- src/calibre/manual/faq.rst | 2 +- 2 files changed, 63 insertions(+), 47 deletions(-) diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py index 5c7d916e1a..4337e558eb 100644 --- a/src/calibre/gui2/wizard/send_email.py +++ b/src/calibre/gui2/wizard/send_email.py @@ -46,6 +46,64 @@ class TestEmail(QDialog, TE_Dialog): finally: self.test_button.setEnabled(True) +class RelaySetup(QDialog): + + def __init__(self, service, parent): + QDialog.__init__(self, parent) + + self.l = l = QGridLayout() + self.setLayout(l) + self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + self.tl = QLabel(('

'+_('Setup sending email using') + + ' {name}

' + + _('If you don\'t have an account, you can sign up for a free {name} email ' + 'account at http://{url}. {extra}')).format( + **service)) + l.addWidget(self.tl, 0, 0, 3, 0) + self.tl.setWordWrap(True) + self.tl.setOpenExternalLinks(True) + for name, label in ( + ['from_', _('Your %s &email address:')], + ['username', _('Your %s &username:')], + ['password', _('Your %s &password:')], + ): + la = QLabel(label%service['name']) + le = QLineEdit(self) + setattr(self, name, le) + setattr(self, name+'_label', la) + r = l.rowCount() + l.addWidget(la, r, 0) + l.addWidget(le, r, 1) + la.setBuddy(le) + if name == 'password': + self.ptoggle = QCheckBox(_('&Show password'), self) + l.addWidget(self.ptoggle, r, 2) + self.ptoggle.stateChanged.connect( + lambda s: self.password.setEchoMode(self.password.Normal if s + == Qt.Checked else self.password.Password)) + self.username.setText(service['username']) + self.password.setEchoMode(self.password.Password) + self.bl = QLabel('

' + _( + 'If you plan to use email to send books to your Kindle, remember to' + ' add the your %s email address to the allowed email addresses in your ' + 'Amazon.com Kindle management page.')%service['name']) + self.bl.setWordWrap(True) + l.addWidget(self.bl, l.rowCount(), 0, 3, 0) + l.addWidget(bb, l.rowCount(), 0, 3, 0) + self.setWindowTitle(_('Setup') + ' ' + service['name']) + self.resize(self.sizeHint()) + self.service = service + + def accept(self): + un = unicode(self.username.text()) + if self.service.get('at_in_username', False) and '@' not in un: + return error_dialog(self, _('Incorrect username'), + _('%s needs the full email address as your username') % + self.service['name'], show=True) + QDialog.accept(self) + class SendEmail(QWidget, Ui_Form): @@ -129,7 +187,8 @@ class SendEmail(QWidget, Ui_Form): 'port': 587, 'username': '@gmail.com', 'url': 'www.gmail.com', - 'extra': '' + 'extra': '', + 'at_in_username': True, }, 'hotmail': { 'name': 'Hotmail', @@ -143,53 +202,10 @@ class SendEmail(QWidget, Ui_Form): ' will let calibre send email. In this case, I' ' strongly suggest you setup a free gmail account' ' instead.'), + 'at_in_username': True, } }[service] - d = QDialog(self) - l = QGridLayout() - d.setLayout(l) - bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) - bb.accepted.connect(d.accept) - bb.rejected.connect(d.reject) - d.tl = QLabel(('

'+_('Setup sending email using') + - ' {name}

' + - _('If you don\'t have an account, you can sign up for a free {name} email ' - 'account at http://{url}. {extra}')).format( - **service)) - l.addWidget(d.tl, 0, 0, 3, 0) - d.tl.setWordWrap(True) - d.tl.setOpenExternalLinks(True) - for name, label in ( - ['from_', _('Your %s &email address:')], - ['username', _('Your %s &username:')], - ['password', _('Your %s &password:')], - ): - la = QLabel(label%service['name']) - le = QLineEdit(d) - setattr(d, name, le) - setattr(d, name+'_label', la) - r = l.rowCount() - l.addWidget(la, r, 0) - l.addWidget(le, r, 1) - la.setBuddy(le) - if name == 'password': - d.ptoggle = QCheckBox(_('&Show password'), d) - l.addWidget(d.ptoggle, r, 2) - d.ptoggle.stateChanged.connect( - lambda s: d.password.setEchoMode(d.password.Normal if s - == Qt.Checked else d.password.Password)) - d.username.setText(service['username']) - d.password.setEchoMode(d.password.Password) - d.bl = QLabel('

' + _( - 'If you plan to use email to send books to your Kindle, remember to' - ' add the your %s email address to the allowed email addresses in your ' - 'Amazon.com Kindle management page.')%service['name']) - d.bl.setWordWrap(True) - l.addWidget(d.bl, l.rowCount(), 0, 3, 0) - l.addWidget(bb, l.rowCount(), 0, 3, 0) - d.setWindowTitle(_('Setup') + ' ' + service['name']) - d.resize(d.sizeHint()) - bb.setVisible(True) + d = RelaySetup(service, self) if d.exec_() != d.Accepted: return self.relay_username.setText(d.username.text()) diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index b120fd4a1b..1c0b49f30b 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -587,7 +587,7 @@ You can download news and convert it into an ebook with the command:: /opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub -If you want to generate MOBI, use outputfile.mobi instead. +If you want to generate MOBI, use outputfile.mobi instead and use ``--output-profile kindle``. You can email downloaded news with the command:: From 4e6c543f75bbaf66a3fd46313c3d91762643b511 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 25 May 2011 19:07:58 -0600 Subject: [PATCH 23/30] Fix #788378 (Add support for nook TSR) --- src/calibre/customize/builtins.py | 7 +++---- src/calibre/devices/nook/driver.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index bbbb8a738f..4a970b4661 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -594,7 +594,7 @@ from calibre.devices.iliad.driver import ILIAD from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800 from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX -from calibre.devices.nook.driver import NOOK, NOOK_COLOR +from calibre.devices.nook.driver import NOOK, NOOK_COLOR, NOOK_TSR from calibre.devices.prs505.driver import PRS505 from calibre.devices.user_defined.driver import USER_DEFINED from calibre.devices.android.driver import ANDROID, S60 @@ -693,8 +693,7 @@ plugins += [ KINDLE, KINDLE2, KINDLE_DX, - NOOK, - NOOK_COLOR, + NOOK, NOOK_COLOR, NOOK_TSR, PRS505, ANDROID, S60, @@ -1277,7 +1276,7 @@ class StoreLegimiStore(StoreBase): author = u'Tomasz Długosz' description = u'Tanie oraz darmowe ebooki, egazety i blogi w formacie EPUB, wprost na Twój e-czytnik, iPhone, iPad, Android i komputer' actual_plugin = 'calibre.gui2.store.legimi_plugin:LegimiStore' - + drm_free_only = False headquarters = 'PL' formats = ['EPUB'] diff --git a/src/calibre/devices/nook/driver.py b/src/calibre/devices/nook/driver.py index 39d0763735..3c30b88568 100644 --- a/src/calibre/devices/nook/driver.py +++ b/src/calibre/devices/nook/driver.py @@ -107,3 +107,13 @@ class NOOK_COLOR(NOOK): return filepath +class NOOK_TSR(NOOK): + gui_name = _('Nook Simple') + description = _('Communicate with the Nook TSR eBook reader.') + + PRODUCT_ID = [0x003] + BCD = [0x216] + + EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'My Files/Books' + + From 612c4c36da96e53b772209e29682fdd1cf409b62 Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 26 May 2011 07:06:15 -0400 Subject: [PATCH 24/30] Store: chooser, add descriptive tool tips. --- src/calibre/gui2/store/config/chooser/models.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py index 2eaa1655ce..6c95d74ffc 100644 --- a/src/calibre/gui2/store/config/chooser/models.py +++ b/src/calibre/gui2/store/config/chooser/models.py @@ -103,7 +103,22 @@ class Matches(QAbstractItemModel): return Qt.Unchecked return Qt.Checked elif role == Qt.ToolTipRole: - return QVariant('

%s

' % result.description) + if col == 0: + if is_disabled(result): + return QVariant(_('

This store is currently diabled and cannot be used in other parts of calibre.

')) + else: + return QVariant(_('

This store is currently enabled and can be used in other parts of calibre.

')) + elif col == 1: + return QVariant('

%s

' % result.description) + elif col == 2: + if result.drm_free_only: + return QVariant(_('

This store only distributes ebooks with DRM.

')) + else: + return QVariant(_('

This store distributes ebooks with DRM. It may have some titles without DRM, but you will need to check on a per title basis.

')) + elif col == 3: + return QVariant(_('

This store is headquartered in %s. This is a good indication of what market the store caters to. However, this does not necessarily mean that the store is limited to that market only.

') % result.headquarters) + elif col == 4: + return QVariant(_('

This store distributes ebooks in the following formats: %s

') % ', '.join(result.formats)) return NONE def setData(self, index, data, role): From 86ad6f7787cada352fc33633865389abbc35e32f Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 26 May 2011 08:29:27 -0400 Subject: [PATCH 25/30] Store: Search, use the GUI object directly to handle istores instead of passing it to the dialog. This to allow for reloading of enabled stores later. --- src/calibre/gui2/actions/store.py | 2 +- src/calibre/gui2/store/search/search.py | 86 +++++++++++-------------- 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py index 0fd783f0a3..6d9720548e 100644 --- a/src/calibre/gui2/actions/store.py +++ b/src/calibre/gui2/actions/store.py @@ -44,7 +44,7 @@ class StoreAction(InterfaceAction): def search(self, query=''): self.show_disclaimer() from calibre.gui2.store.search.search import SearchDialog - sd = SearchDialog(self.gui.istores, self.gui, query) + sd = SearchDialog(self.gui, self.gui, query) sd.exec_() def _get_selected_row(self): diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 3be39e3e87..fd49ebd67b 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -10,7 +10,7 @@ import re from random import shuffle from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, - QVBoxLayout, QIcon, QWidget) + QVBoxLayout, QIcon, QWidget, QTabWidget) from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator @@ -22,7 +22,7 @@ from calibre.gui2.store.search.search_ui import Ui_Dialog class SearchDialog(QDialog, Ui_Dialog): - def __init__(self, istores, parent=None, query=''): + def __init__(self, gui, parent=None, query=''): QDialog.__init__(self, parent) self.setupUi(self) @@ -34,8 +34,7 @@ class SearchDialog(QDialog, Ui_Dialog): # the variables it sets up are used later. self.load_settings() - # We keep a cache of store plugins and reference them by name. - self.store_plugins = istores + self.gui = gui # Setup our worker threads. self.search_pool = SearchThreadPool(self.search_thread_count) @@ -49,22 +48,11 @@ class SearchDialog(QDialog, Ui_Dialog): self.hang_check = 0 # Update store caches silently. - for p in self.store_plugins.values(): + for p in self.gui.istores.values(): self.cache_pool.add_task(p, self.timeout) - # Add check boxes for each store so the user - # can disable searching specific stores on a - # per search basis. - stores_check_widget = QWidget() - store_list_layout = QVBoxLayout() - stores_check_widget.setLayout(store_list_layout) - for x in sorted(self.store_plugins.keys(), key=lambda x: x.lower()): - cbox = QCheckBox(x) - cbox.setChecked(False) - store_list_layout.addWidget(cbox) - setattr(self, 'store_check_' + x, cbox) - store_list_layout.addStretch() - self.store_list.setWidget(stores_check_widget) + self.store_checks = {} + self.setup_store_checks() # Set the search query self.search_edit.setText(query) @@ -91,6 +79,22 @@ class SearchDialog(QDialog, Ui_Dialog): self.progress_checker.start(100) self.restore_state() + + def setup_store_checks(self): + # Add check boxes for each store so the user + # can disable searching specific stores on a + # per search basis. + stores_check_widget = QWidget() + store_list_layout = QVBoxLayout() + stores_check_widget.setLayout(store_list_layout) + for x in sorted(self.gui.istores.keys(), key=lambda x: x.lower()): + cbox = QCheckBox(x) + cbox.setChecked(False) + store_list_layout.addWidget(cbox) + self.store_checks['store_check_' + x] = cbox + store_list_layout.addStretch() + self.store_list.setWidget(stores_check_widget) + def build_adv_search(self): adv = AdvSearchBuilderDialog(self) @@ -126,11 +130,12 @@ class SearchDialog(QDialog, Ui_Dialog): # futher filtering. self.results_view.model().set_query(query) - # Plugins are in alphebetic order. Randomize the - # order of plugin names. This way plugins closer + # Plugins are in random order that does not change. + # Randomize the ord of the plugin names every time + # there is a search. This way plugins closer # to a don't have an unfair advantage over # plugins further from a. - store_names = self.store_plugins.keys() + store_names = self.store_checks.keys() if not store_names: return # Remove all of our internal filtering logic from the query. @@ -138,8 +143,8 @@ class SearchDialog(QDialog, Ui_Dialog): shuffle(store_names) # Add plugins that the user has checked to the search pool's work queue. for n in store_names: - if getattr(self, 'store_check_' + n).isChecked(): - self.search_pool.add_task(query, n, self.store_plugins[n], self.max_results, self.timeout) + if self.store_checks[n].isChecked(): + self.search_pool.add_task(query, n, self.gui.istores[n], self.max_results, self.timeout) self.hang_check = 0 self.checker.start(100) self.pi.startAnimation() @@ -179,8 +184,8 @@ class SearchDialog(QDialog, Ui_Dialog): self.config['open_external'] = self.open_external.isChecked() store_check = {} - for n in self.store_plugins: - store_check[n] = getattr(self, 'store_check_' + n).isChecked() + for k, v in self.store_checks.items(): + store_check[k] = v.isChecked() self.config['store_checked'] = store_check def restore_state(self): @@ -206,8 +211,8 @@ class SearchDialog(QDialog, Ui_Dialog): store_check = self.config.get('store_checked', None) if store_check: for n in store_check: - if hasattr(self, 'store_check_' + n): - getattr(self, 'store_check_' + n).setChecked(store_check[n]) + if n in self.store_checks: + self.store_checks[n].setChecked(store_check[n]) self.results_view.model().sort_col = self.config.get('sort_col', 2) self.results_view.model().sort_order = self.config.get('sort_order', Qt.AscendingOrder) @@ -234,7 +239,7 @@ class SearchDialog(QDialog, Ui_Dialog): self.config['open_external'] = self.open_external.isChecked() d = QDialog(self) - button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box = QDialogButtonBox(QDialogButtonBox.Close) v = QVBoxLayout(d) button_box.accepted.connect(d.accept) button_box.rejected.connect(d.reject) @@ -244,10 +249,8 @@ class SearchDialog(QDialog, Ui_Dialog): v.addWidget(button_box) d.exec_() - - if d.result() == QDialog.Accepted: - config_widget.save_settings() - self.config_changed() + config_widget.save_settings() + self.config_changed() def config_changed(self): self.load_settings() @@ -283,7 +286,7 @@ class SearchDialog(QDialog, Ui_Dialog): def open_store(self, index): result = self.results_view.model().get_result(index) - self.store_plugins[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) + self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) def check_progress(self): if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running(): @@ -292,27 +295,16 @@ class SearchDialog(QDialog, Ui_Dialog): if not self.pi.isAnimated(): self.pi.startAnimation() - def get_store_checks(self): - ''' - Returns a list of QCheckBox's for each store. - ''' - checks = [] - for x in self.store_plugins: - check = getattr(self, 'store_check_' + x, None) - if check: - checks.append(check) - return checks - def stores_select_all(self): - for check in self.get_store_checks(): + for check in self.store_checks.values(): check.setChecked(True) def stores_select_invert(self): - for check in self.get_store_checks(): + for check in self.store_checks.values(): check.setChecked(not check.isChecked()) def stores_select_none(self): - for check in self.get_store_checks(): + for check in self.store_checks.values(): check.setChecked(False) def dialog_closed(self, result): From 0a18bb39060df5c20a93f433e3ddebc6b4766f00 Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 26 May 2011 08:45:04 -0400 Subject: [PATCH 26/30] Store: Search, integrate store chooser into config dialog. --- src/calibre/gui2/store/search/search.py | 27 +++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index fd49ebd67b..faeaf507c9 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -14,6 +14,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator +from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget from calibre.gui2.store.config.search.search_widget import StoreConfigWidget from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.search.download_thread import SearchThreadPool, \ @@ -84,18 +85,23 @@ class SearchDialog(QDialog, Ui_Dialog): # Add check boxes for each store so the user # can disable searching specific stores on a # per search basis. + existing = {} + for n in self.store_checks: + existing[n] = self.store_checks[n].isChecked() + + self.store_checks = {} + stores_check_widget = QWidget() store_list_layout = QVBoxLayout() stores_check_widget.setLayout(store_list_layout) for x in sorted(self.gui.istores.keys(), key=lambda x: x.lower()): cbox = QCheckBox(x) - cbox.setChecked(False) + cbox.setChecked(existing.get(x, False)) store_list_layout.addWidget(cbox) - self.store_checks['store_check_' + x] = cbox + self.store_checks[x] = cbox store_list_layout.addStretch() self.store_list.setWidget(stores_check_widget) - def build_adv_search(self): adv = AdvSearchBuilderDialog(self) if adv.exec_() == QDialog.Accepted: @@ -244,13 +250,22 @@ class SearchDialog(QDialog, Ui_Dialog): button_box.accepted.connect(d.accept) button_box.rejected.connect(d.reject) d.setWindowTitle(_('Customize get books search')) - config_widget = StoreConfigWidget(self.config) - v.addWidget(config_widget) + + tab_widget = QTabWidget(d) + v.addWidget(tab_widget) v.addWidget(button_box) + chooser_config_widget = StoreChooserWidget() + search_config_widget = StoreConfigWidget(self.config) + + tab_widget.addTab(chooser_config_widget, _('Choose stores')) + tab_widget.addTab(search_config_widget, _('Configure search')) + d.exec_() - config_widget.save_settings() + search_config_widget.save_settings() self.config_changed() + self.gui.load_store_plugins() + self.setup_store_checks() def config_changed(self): self.load_settings() From ea0920053f9c89909b70ebaf084b9b0fc0ea7950 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 May 2011 10:50:57 -0600 Subject: [PATCH 27/30] Fix #774849 (Schedule news download: inconsistent save behaviour) --- src/calibre/gui2/dialogs/scheduler.py | 18 +++--- src/calibre/gui2/dialogs/scheduler.ui | 79 ++++++++++++++++----------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index b25d66979d..7d1d87b472 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -207,8 +207,9 @@ class SchedulerDialog(QDialog, Ui_Dialog): self.recipe_model.searched.connect(self.search.search_done, type=Qt.QueuedConnection) self.recipe_model.searched.connect(self.search_done) - self.search.setFocus(Qt.OtherFocusReason) + self.recipes.setFocus(Qt.OtherFocusReason) self.commit_on_change = True + self.previous_urn = None self.recipes.setModel(self.recipe_model) self.detail_box.setVisible(False) @@ -228,6 +229,9 @@ class SchedulerDialog(QDialog, Ui_Dialog): self.old_news.setValue(gconf['oldest_news']) + self.go_button.clicked.connect(self.search.do_search) + self.clear_search_button.clicked.connect(self.search.clear_clicked) + def set_pw_echo_mode(self, state): self.password.setEchoMode(self.password.Normal if state == Qt.Checked else self.password.Password) @@ -265,14 +269,9 @@ class SchedulerDialog(QDialog, Ui_Dialog): self.last_downloaded.setVisible(enabled) def current_changed(self, current, previous): - if self.commit_on_change: - if previous.isValid(): - if not self.commit(urn=getattr(previous.internalPointer(), - 'urn', None)): - self.commit_on_change = False - self.recipes.setCurrentIndex(previous) - else: - self.commit_on_change = True + if self.previous_urn is not None: + self.commit(urn=self.previous_urn) + self.previous_urn = None urn = self.current_urn if urn is not None: @@ -332,6 +331,7 @@ class SchedulerDialog(QDialog, Ui_Dialog): return True def initialize_detail_box(self, urn): + self.previous_urn = urn self.detail_box.setVisible(True) self.download_button.setVisible(True) self.detail_box.setCurrentIndex(0) diff --git a/src/calibre/gui2/dialogs/scheduler.ui b/src/calibre/gui2/dialogs/scheduler.ui index f26bfc7285..6acbb01dd8 100644 --- a/src/calibre/gui2/dialogs/scheduler.ui +++ b/src/calibre/gui2/dialogs/scheduler.ui @@ -17,21 +17,30 @@ :/images/scheduler.png:/images/scheduler.png - + - - - &Search: - - - search - - + + + + + + + + Go + + + + + + + + :/images/clear_left.png:/images/clear_left.png + + + + - - - - + QFrame::NoFrame @@ -44,7 +53,7 @@ 0 0 - 486 + 524 504 @@ -320,7 +329,7 @@ - + @@ -345,7 +354,17 @@ - + + + + + + + Qt::AlignCenter + + + + @@ -376,17 +395,7 @@ - - - - Qt::Horizontal - - - QDialogButtonBox::Save - - - - + Download all scheduled news sources at once @@ -394,15 +403,19 @@ Download &all scheduled + + + :/images/news.png:/images/news.png + - - - - + + + + Qt::Horizontal - - Qt::AlignCenter + + QDialogButtonBox::Save From 5744242319e151fd7ac6292afb2eef9967520cca Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 May 2011 10:58:48 -0600 Subject: [PATCH 28/30] Fix #788570 (Calibre Epub to Mobi does not handle hidden spans correctly) --- src/calibre/ebooks/mobi/mobiml.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 3c36a6166d..2275552c15 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -297,9 +297,11 @@ class MobiMLizer(object): if id_: # Keep anchors so people can use display:none # to generate hidden TOCs + tail = elem.tail elem.clear() elem.text = None elem.set('id', id_) + elem.tail = tail else: return tag = barename(elem.tag) @@ -309,7 +311,8 @@ class MobiMLizer(object): istates.append(istate) left = 0 display = style['display'] - isblock = not display.startswith('inline') + isblock = (not display.startswith('inline') and style['display'] != + 'none') isblock = isblock and style['float'] == 'none' isblock = isblock and tag != 'br' if isblock: From 87dec0e0640fb1013c55b0a2f2debad7963139d2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 May 2011 11:46:35 -0600 Subject: [PATCH 29/30] Update faz.net --- recipes/faznet.recipe | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/recipes/faznet.recipe b/recipes/faznet.recipe index 01a46d43ba..50a66c59b8 100644 --- a/recipes/faznet.recipe +++ b/recipes/faznet.recipe @@ -3,10 +3,7 @@ class AdvancedUserRecipe1303841067(BasicNewsRecipe): title = u'Faz.net' __author__ = 'schuster' - remove_tags = [dict(attrs={'class':['right', 'ArrowLinkRight', 'ModulVerlagsInfo', 'left', 'Head']}), - dict(id=['BreadCrumbs', 'tstag', 'FazFooterPrint']), - dict(name=['script', 'noscript', 'style'])] - oldest_article = 2 + oldest_article = 1 description = 'Frankfurter Allgemeine Zeitung' max_articles_per_feed = 100 no_stylesheets = True @@ -15,9 +12,9 @@ class AdvancedUserRecipe1303841067(BasicNewsRecipe): remove_javascript = True cover_url = 'http://www.faz.net/f30/Images/Logos/logo.gif' - def print_version(self, url): - return url.replace('.html', '~Afor~Eprint.html') - + remove_tags = [dict(attrs={'class':['LinkBoxModulSmall', 'ModulLesermeinungenFooter', 'ModulArtikelServices', 'SocialMediaUnten', 'ArrowLinkRight', 'ModulVerlagsInfo', 'AdData', 'FazFooter', 'Date']}), + dict(id=['FAZNavHeader', 'FAZNavMain', 'RightColumn', 'FazFooter', 'BreadCrumbs', 'FAZNavSubMain', 'FAZImgEvent']), + dict(name=['jksrdt'])] feeds = [(u'Politik', u'http://www.faz.net/s/RubA24ECD630CAE40E483841DB7D16F4211/Tpl~Epartner~SRss_.xml'), From d2036d1c3043caaf56e1cc968feace1ee3de9885 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 May 2011 13:31:04 -0600 Subject: [PATCH 30/30] Various German news sources by schuster --- recipes/aachener_nachrichten.recipe | 42 +++++++++++++++++++++ recipes/frankfurter_rundschau.recipe | 35 ++++++++++++++++++ recipes/rheinische_post.recipe | 55 ++++++++++++++++++++++++++++ resources/template-functions.json | 15 +++++--- 4 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 recipes/aachener_nachrichten.recipe create mode 100644 recipes/frankfurter_rundschau.recipe create mode 100644 recipes/rheinische_post.recipe diff --git a/recipes/aachener_nachrichten.recipe b/recipes/aachener_nachrichten.recipe new file mode 100644 index 0000000000..a2294fc472 --- /dev/null +++ b/recipes/aachener_nachrichten.recipe @@ -0,0 +1,42 @@ +from calibre.web.feeds.recipes import BasicNewsRecipe +class AdvancedUserRecipe(BasicNewsRecipe): + + title = u'Aachener Nachrichten' + __author__ = 'schuster' + oldest_article = 1 + max_articles_per_feed = 100 + use_embedded_content = False + language = 'de' + remove_javascript = True + cover_url = 'http://www.an-online.de/einwaage/images/an_logo.png' + masthead_url = 'http://www.an-online.de/einwaage/images/an_logo.png' + extra_css = ''' + .fliesstext_detail:{margin-bottom:10%;} + .headline_1:{margin-bottom:25%;} + b{font-family:Arial,Helvetica,sans-serif; font-weight:200;font-size:large;} + a{font-family:Arial,Helvetica,sans-serif; font-weight:400;font-size:large;} + ll{font-family:Arial,Helvetica,sans-serif; font-weight:100;font-size:large;} + h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} + img {min-width:300px; max-width:600px; min-height:300px; max-height:800px} + dd{font-family:Arial,Helvetica,sans-serif;font-size:large;} + body{font-family:Helvetica,Arial,sans-serif;font-size:small;} + ''' + + + + keep_only_tags = [ + dict(name='span', attrs={'class':['fliesstext_detail', 'headline_1', 'autor_detail']}), + dict(id=['header-logo']) + ] + + feeds = [(u'Euregio', u'http://www.an-online.de/an/rss/Euregio.xml'), + (u'Aachen', u'http://www.an-online.de/an/rss/Aachen.xml'), + (u'Nordkreis', u'http://www.an-online.de/an/rss/Nordkreis.xml'), + (u'Düren', u'http://www.an-online.de/an/rss/Dueren.xml'), + (u'Eiffel', u'http://www.an-online.de/an/rss/Eifel.xml'), + (u'Eschweiler', u'http://www.an-online.de/an/rss/Eschweiler.xml'), + (u'Geilenkirchen', u'http://www.an-online.de/an/rss/Geilenkirchen.xml'), + (u'Heinsberg', u'http://www.an-online.de/an/rss/Heinsberg.xml'), + (u'Jülich', u'http://www.an-online.de/an/rss/Juelich.xml'), + (u'Stolberg', u'http://www.an-online.de/an/rss/Stolberg.xml'), + (u'Ratgebenr', u'http://www.an-online.de/an/rss/Ratgeber.xml')] diff --git a/recipes/frankfurter_rundschau.recipe b/recipes/frankfurter_rundschau.recipe new file mode 100644 index 0000000000..3c3bb32ca3 --- /dev/null +++ b/recipes/frankfurter_rundschau.recipe @@ -0,0 +1,35 @@ +from calibre.web.feeds.recipes import BasicNewsRecipe +class AdvancedUserRecipe(BasicNewsRecipe): + + title = u'Frankfurter Rundschau' + __author__ = 'schuster' + oldest_article = 1 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + language = 'de' + remove_javascript = True + cover_url = 'http://www.fr-online.de/image/view/-/1474018/data/823538/-/logo.png' + extra_css = ''' + h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;} + h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} + img {min-width:300px; max-width:600px; min-height:300px; max-height:800px} + p{font-family:Arial,Helvetica,sans-serif;font-size:small;} + body{font-family:Helvetica,Arial,sans-serif;font-size:small;} + ''' + + feeds = [(u'Startseite', u'http://www.fr-online.de/home/-/1472778/1472778/-/view/asFeed/-/index.xml'), + (u'Politik', u'http://www.fr-online.de/politik/-/1472596/1472596/-/view/asFeed/-/index.xml'), + (u'Meinungen', u'http://www.fr-online.de/politik/meinung/-/1472602/1472602/-/view/asFeed/-/index.xml'), + (u'Wirtschaft', u'http://www.fr-online.de/wirtschaft/-/1472780/1472780/-/view/asFeed/-/index.xml'), + (u'Sport', u'http://www.fr-online.de/sport/-/1472784/1472784/-/view/asFeed/-/index.xml'), + (u'Kultur', u'http://www.fr-online.de/kultur/-/1472786/1472786/-/view/asFeed/-/index.xml'), + (u'Panorama', u'http://www.fr-online.de/panorama/-/1472782/1472782/-/view/asFeed/-/index.xml'), + (u'Digital', u'http://www.fr-online.de/digital/-/1472406/1472406/-/view/asFeed/-/index.xml'), + (u'Wissenschaft', u'http://www.fr-online.de/wissenschaft/-/1472788/1472788/-/view/asFeed/-/index.xml') +] + + + def print_version(self, url): + return url.replace('index.html', 'view/printVersion/-/index.html') + diff --git a/recipes/rheinische_post.recipe b/recipes/rheinische_post.recipe new file mode 100644 index 0000000000..1d3efc710d --- /dev/null +++ b/recipes/rheinische_post.recipe @@ -0,0 +1,55 @@ +from calibre.web.feeds.recipes import BasicNewsRecipe +class AdvancedUserRecipe(BasicNewsRecipe): + + title = u'RP-online' + __author__ = 'schuster' + oldest_article = 2 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + language = 'de' + remove_javascript = True + masthead_url = 'http://www.die-zeitungen.de/uploads/pics/LOGO_RP_ONLINE_01.jpg' + cover_url = 'http://www.manroland.com/com/pressinfo_images/com/RheinischePost_Logo_300dpi.jpg' + extra_css = ''' + h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;} + h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} + img {min-width:300px; max-width:600px; min-height:300px; max-height:800px} + p{font-family:Arial,Helvetica,sans-serif;font-size:small;} + body{font-family:Helvetica,Arial,sans-serif;font-size:small;} + ''' + remove_tags_before = dict(id='article_content') + remove_tags_after = dict(id='article_content') + remove_tags = [dict(attrs={'class':['goodies', 'left', 'right', 'clear-all', 'teaser anzeigenwerbung', 'lesermeinung', 'goodiebox', 'goodiebox 1', 'goodiebox 2', 'goodiebox 3', 'boxframe', 'link']}), + dict(id=['click_Fotos_link']), + dict(name=['script', 'noscript', 'style', '_top', 'click_Fotos_link'])] + + feeds = [ (u'Top-News', u'http://www.ngz-online.de/app/feed/rss/topnews'), + (u'Politik', u'http://www.ngz-online.de/app/feed/rss/politik'), + (u'Wirtschaft', u'http://www.ngz-online.de/app/feed/rss/wirtschaft'), + (u'Panorama', u'http://www.ngz-online.de/app/feed/rss/panorama'), + (u'Sport', u'http://www.ngz-online.de/app/feed/rss/sport'), + (u'Tour de France', u'http://www.ngz-online.de/app/feed/rss/tourdefrance'), + (u'Fußball', u'http://www.ngz-online.de/app/feed/rss/fussball'), + (u'Fußball BuLi', u'http://www.ngz-online.de/app/feed/rss/bundesliga'), + (u'Formel 1', u'http://www.ngz-online.de/app/feed/rss/formel1'), + (u'US-Sport', u'http://www.ngz-online.de/app/feed/rss/us-sports'), + (u'Boxen', u'http://www.ngz-online.de/app/feed/rss/boxen'), + (u'Eishockey', u'http://www.ngz-online.de/app/feed/rss/eishockey'), + (u'Basketball', u'http://www.ngz-online.de/app/feed/rss/basketball'), + (u'Handball', u'http://www.ngz-online.de/app/feed/rss/handball'), + (u'Motorsport', u'http://www.ngz-online.de/app/feed/rss/motorsport'), + (u'Tennis', u'http://www.ngz-online.de/app/feed/rss/tennis'), + (u'Radsport', u'http://www.ngz-online.de/app/feed/rss/radsport'), + (u'Kultur', u'http://www.ngz-online.de/app/feed/rss/kultur'), + (u'Gesellschaft', u'http://www.ngz-online.de/app/feed/rss/gesellschaft'), + (u'Wissenschaft', u'http://www.ngz-online.de/app/feed/rss/wissen'), + (u'Gesundheit', u'http://www.ngz-online.de/app/feed/rss/gesundheit'), + (u'Digitale Welt', u'http://www.ngz-online.de/app/feed/rss/digitale'), + (u'Auto & Mobil', u'http://www.ngz-online.de/app/feed/rss/auto'), + (u'Reise & Welt', u'http://www.ngz-online.de/app/feed/rss/reise'), + (u'Beruf & Karriere', u'http://www.ngz-online.de/app/feed/rss/beruf'), + (u'Herzrasen', u'http://www.ngz-online.de/app/feed/rss/herzrasen'), + (u'About a Boy', u'http://www.ngz-online.de/app/feed/rss/about_a_boy'), + +] diff --git a/resources/template-functions.json b/resources/template-functions.json index 7656db4021..b0a2225dd4 100644 --- a/resources/template-functions.json +++ b/resources/template-functions.json @@ -1,19 +1,21 @@ { + "and": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if not args[i]:\n return ''\n i += 1\n return '1'\n", "contains": "def evaluate(self, formatter, kwargs, mi, locals,\n val, test, value_if_present, value_if_not):\n if re.search(test, val):\n return value_if_present\n else:\n return value_if_not\n", "divide": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x / y)\n", "uppercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.upper()\n", "strcat": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n res = ''\n for i in range(0, len(args)):\n res += args[i]\n return res\n", "in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n for v in l:\n if re.search(pat, v):\n return fv\n return nfv\n", - "substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n", + "multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n", "ifempty": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):\n if val:\n return val\n else:\n return value_if_empty\n", "booksize": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.book_size is not None:\n try:\n return str(mi.book_size)\n except:\n pass\n return ''\n", "select": "def evaluate(self, formatter, kwargs, mi, locals, val, key):\n if not val:\n return ''\n vals = [v.strip() for v in val.split(',')]\n for v in vals:\n if v.startswith(key+':'):\n return v[len(key)+1:]\n return ''\n", + "strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n", "first_non_empty": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return args[i]\n i += 1\n return ''\n", - "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n", + "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val)\n", "subtract": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x - y)\n", "list_item": "def evaluate(self, formatter, kwargs, mi, locals, val, index, sep):\n if not val:\n return ''\n index = int(index)\n val = val.split(sep)\n try:\n return val[index]\n except:\n return ''\n", "shorten": "def evaluate(self, formatter, kwargs, mi, locals,\n val, leading, center_string, trailing):\n l = max(0, int(leading))\n t = max(0, int(trailing))\n if len(val) > l + len(center_string) + t:\n return val[0:l] + center_string + ('' if t == 0 else val[-t:])\n else:\n return val\n", - "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val)\n", + "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n", "add": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x + y)\n", "lookup": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if len(args) == 2: # here for backwards compatibility\n if val:\n return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)\n else:\n return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)\n if (len(args) % 2) != 1:\n raise ValueError(_('lookup requires either 2 or an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)\n if re.search(args[i], val):\n return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)\n i += 2\n", "template": "def evaluate(self, formatter, kwargs, mi, locals, template):\n template = template.replace('[[', '{').replace(']]', '}')\n return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)\n", @@ -23,14 +25,15 @@ "sublist": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n val = val.split(sep)\n try:\n if ei == 0:\n return sep.join(val[si:])\n else:\n return sep.join(val[si:ei])\n except:\n return ''\n", "test": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set):\n if val:\n return value_if_set\n else:\n return value_not_set\n", "eval": "def evaluate(self, formatter, kwargs, mi, locals, template):\n from formatter import eval_formatter\n template = template.replace('[[', '{').replace(']]', '}')\n return eval_formatter.safe_format(template, locals, 'EVAL', None)\n", - "multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n", + "not": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n", "format_date": "def evaluate(self, formatter, kwargs, mi, locals, val, format_string):\n if not val:\n return ''\n try:\n dt = parse_date(val)\n s = format_date(dt, format_string)\n except:\n s = 'BAD DATE'\n return s\n", "capitalize": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return capitalize(val)\n", "count": "def evaluate(self, formatter, kwargs, mi, locals, val, sep):\n return unicode(len(val.split(sep)))\n", "lowercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.lower()\n", - "strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n", - "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n", + "substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n", "assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n", + "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n", + "or": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n", "raw_field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return unicode(getattr(mi, name, None))\n", "cmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n x = float(x if x else 0)\n y = float(y if y else 0)\n if x < y:\n return lt\n if x == y:\n return eq\n return gt\n" } \ No newline at end of file