This commit is contained in:
Kovid Goyal 2011-04-22 07:46:49 -06:00
commit 24ea3c4327
22 changed files with 826 additions and 792 deletions

View File

@ -33,7 +33,7 @@ class StoreAction(InterfaceAction):
def search(self): def search(self):
self.show_disclaimer() self.show_disclaimer()
from calibre.gui2.store.search import SearchDialog from calibre.gui2.store.search.search import SearchDialog
sd = SearchDialog(self.gui.istores, self.gui) sd = SearchDialog(self.gui.istores, self.gui)
sd.exec_() sd.exec_()

View File

@ -46,9 +46,12 @@ class StorePlugin(object): # {{{
''' '''
def __init__(self, gui, name): def __init__(self, gui, name):
from calibre.gui2 import JSONConfig
self.gui = gui self.gui = gui
self.name = name self.name = name
self.base_plugin = None self.base_plugin = None
self.config = JSONConfig('store/stores/' + self.name)
def open(self, gui, parent=None, detail_item=None, external=False): def open(self, gui, parent=None, detail_item=None, external=False):
''' '''

View File

@ -8,14 +8,8 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QWidget from PyQt4.Qt import QWidget
from calibre.gui2 import gprefs
from calibre.gui2.store.basic_config_widget_ui import Ui_Form from calibre.gui2.store.basic_config_widget_ui import Ui_Form
def save_settings(config_widget):
gprefs[config_widget.store.name + '_open_external'] = config_widget.open_external.isChecked()
tags = unicode(config_widget.tags.text())
gprefs[config_widget.store.name + '_tags'] = tags
class BasicStoreConfigWidget(QWidget, Ui_Form): class BasicStoreConfigWidget(QWidget, Ui_Form):
def __init__(self, store): def __init__(self, store):
@ -27,10 +21,10 @@ class BasicStoreConfigWidget(QWidget, Ui_Form):
self.load_setings() self.load_setings()
def load_setings(self): def load_setings(self):
settings = self.store.get_settings() config = self.store.config
self.open_external.setChecked(settings.get(self.store.name + '_open_external')) self.open_external.setChecked(config.get('open_external', False))
self.tags.setText(settings.get(self.store.name + '_tags', '')) self.tags.setText(config.get('tags', ''))
class BasicStoreConfig(object): class BasicStoreConfig(object):
@ -41,12 +35,6 @@ class BasicStoreConfig(object):
return BasicStoreConfigWidget(self) return BasicStoreConfigWidget(self)
def save_settings(self, config_widget): def save_settings(self, config_widget):
save_settings(config_widget) self.config['open_external'] = config_widget.open_external.isChecked()
tags = unicode(config_widget.tags.text())
def get_settings(self): self.config['tags'] = tags
settings = {}
settings[self.name + '_open_external'] = gprefs.get(self.name + '_open_external', False)
settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download')
return settings

View File

@ -23,10 +23,9 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class BeWriteStore(BasicStoreConfig, StorePlugin): class BeWriteStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT' url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
if detail_item: if detail_item:
url = url + detail_item url = url + detail_item
open_url(QUrl(url_slash_cleaner(url))) open_url(QUrl(url_slash_cleaner(url)))
@ -36,7 +35,7 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item detail_url = url + detail_item
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -25,8 +25,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class BNStore(BasicStoreConfig, StorePlugin): class BNStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
pub_id = '21000000000352219' pub_id = '21000000000352219'
# Use Kovid's affiliate id 30% of the time. # Use Kovid's affiliate id 30% of the time.
if random.randint(1, 10) in (1, 2, 3): if random.randint(1, 10) in (1, 2, 3):
@ -40,12 +38,12 @@ class BNStore(BasicStoreConfig, StorePlugin):
isbn = mo.group('isbn') isbn = mo.group('isbn')
detail_item = 'http://gan.doubleclick.net/gan_click?lid=41000000012871747&pid=' + isbn + '&adurl=' + detail_item + '&pubid=' + pub_id detail_item = 'http://gan.doubleclick.net/gan_click?lid=41000000012871747&pid=' + isbn + '&adurl=' + detail_item + '&pubid=' + pub_id
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
else: else:
d = WebStoreDialog(self.gui, url, parent, detail_item) d = WebStoreDialog(self.gui, url, parent, detail_item)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -24,7 +24,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class DieselEbooksStore(BasicStoreConfig, StorePlugin): class DieselEbooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.diesel-ebooks.com/' url = 'http://www.diesel-ebooks.com/'
aff_id = '?aid=2049' aff_id = '?aid=2049'
@ -37,12 +36,12 @@ class DieselEbooksStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item + aff_id detail_url = url + detail_item + aff_id
url = url + aff_id url = url + aff_id
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else: else:
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -25,8 +25,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class EbookscomStore(BasicStoreConfig, StorePlugin): class EbookscomStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
m_url = 'http://www.dpbolvw.net/' m_url = 'http://www.dpbolvw.net/'
h_click = 'click-4879827-10364500' h_click = 'click-4879827-10364500'
d_click = 'click-4879827-10281551' d_click = 'click-4879827-10281551'
@ -40,12 +38,12 @@ class EbookscomStore(BasicStoreConfig, StorePlugin):
if detail_item: if detail_item:
detail_url = m_url + d_click + detail_item detail_url = m_url + d_click + detail_item
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else: else:
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -25,8 +25,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class EHarlequinStore(BasicStoreConfig, StorePlugin): class EHarlequinStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
m_url = 'http://www.dpbolvw.net/' m_url = 'http://www.dpbolvw.net/'
h_click = 'click-4879827-534091' h_click = 'click-4879827-534091'
d_click = 'click-4879827-10375439' d_click = 'click-4879827-10375439'
@ -40,12 +38,12 @@ class EHarlequinStore(BasicStoreConfig, StorePlugin):
if detail_item: if detail_item:
detail_url = m_url + d_click + detail_item detail_url = m_url + d_click + detail_item
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else: else:
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -23,11 +23,10 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class FeedbooksStore(BasicStoreConfig, StorePlugin): class FeedbooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://m.feedbooks.com/' url = 'http://m.feedbooks.com/'
ext_url = 'http://feedbooks.com/' ext_url = 'http://feedbooks.com/'
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
if detail_item: if detail_item:
ext_url = ext_url + detail_item ext_url = ext_url + detail_item
open_url(QUrl(url_slash_cleaner(ext_url))) open_url(QUrl(url_slash_cleaner(ext_url)))
@ -37,7 +36,7 @@ class FeedbooksStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item detail_url = url + detail_item
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -23,11 +23,10 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class GutenbergStore(BasicStoreConfig, StorePlugin): class GutenbergStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://m.gutenberg.org/' url = 'http://m.gutenberg.org/'
ext_url = 'http://gutenberg.org/' ext_url = 'http://gutenberg.org/'
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
if detail_item: if detail_item:
ext_url = ext_url + detail_item ext_url = ext_url + detail_item
open_url(QUrl(url_slash_cleaner(ext_url))) open_url(QUrl(url_slash_cleaner(ext_url)))
@ -37,7 +36,7 @@ class GutenbergStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item detail_url = url + detail_item
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -24,8 +24,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class KoboStore(BasicStoreConfig, StorePlugin): class KoboStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
m_url = 'http://www.dpbolvw.net/' m_url = 'http://www.dpbolvw.net/'
h_click = 'click-4879827-10762497' h_click = 'click-4879827-10762497'
d_click = 'click-4879827-10772898' d_click = 'click-4879827-10772898'
@ -39,12 +37,12 @@ class KoboStore(BasicStoreConfig, StorePlugin):
if detail_item: if detail_item:
detail_url = m_url + d_click + detail_item detail_url = m_url + d_click + detail_item
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else: else:
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -24,19 +24,18 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class ManyBooksStore(BasicStoreConfig, StorePlugin): class ManyBooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://manybooks.net/' url = 'http://manybooks.net/'
detail_url = None detail_url = None
if detail_item: if detail_item:
detail_url = url + detail_item detail_url = url + detail_item
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else: else:
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -18,7 +18,7 @@ from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVarian
pyqtSignal pyqtSignal
from calibre import browser from calibre import browser
from calibre.gui2 import open_url, NONE, JSONConfig from calibre.gui2 import open_url, NONE
from calibre.gui2.store import StorePlugin from calibre.gui2.store import StorePlugin
from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.basic_config import BasicStoreConfig
from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog
@ -29,20 +29,18 @@ from calibre.utils.icu import sort_key
class MobileReadStore(BasicStoreConfig, StorePlugin): class MobileReadStore(BasicStoreConfig, StorePlugin):
def genesis(self): def genesis(self):
self.config = JSONConfig('store/store/' + self.name)
self.rlock = RLock() self.rlock = RLock()
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.mobileread.com/' url = 'http://www.mobileread.com/'
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(detail_item if detail_item else url)) open_url(QUrl(detail_item if detail_item else url))
else: else:
if detail_item: if detail_item:
d = WebStoreDialog(self.gui, url, parent, detail_item) d = WebStoreDialog(self.gui, url, parent, detail_item)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
else: else:
d = MobeReadStoreDialog(self, parent) d = MobeReadStoreDialog(self, parent)

View File

@ -23,10 +23,9 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class OpenLibraryStore(BasicStoreConfig, StorePlugin): class OpenLibraryStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://openlibrary.org/' url = 'http://openlibrary.org/'
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
if detail_item: if detail_item:
url = url + detail_item url = url + detail_item
open_url(QUrl(url_slash_cleaner(url))) open_url(QUrl(url_slash_cleaner(url)))
@ -36,7 +35,7 @@ class OpenLibraryStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item detail_url = url + detail_item
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -1,726 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import re
import time
import traceback
from contextlib import closing
from operator import attrgetter
from random import shuffle
from threading import Thread
from Queue import Queue
from PyQt4.Qt import (Qt, QAbstractItemModel, QDialog, QTimer, QVariant,
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout)
from calibre import browser
from calibre.gui2 import NONE, JSONConfig
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.store.search_ui import Ui_Dialog
from calibre.gui2.store.search_result import SearchResult
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH
from calibre.utils.icu import sort_key
from calibre.utils.magick.draw import thumbnail
from calibre.utils.search_query_parser import SearchQueryParser
HANG_TIME = 75000 # milliseconds seconds
TIMEOUT = 75 # seconds
SEARCH_THREAD_TOTAL = 4
COVER_DOWNLOAD_THREAD_TOTAL = 2
def comparable_price(text):
if len(text) < 3 or text[-3] not in ('.', ','):
text += '00'
text = re.sub(r'\D', '', text)
text = text.rjust(6, '0')
return text
class SearchDialog(QDialog, Ui_Dialog):
def __init__(self, istores, *args):
QDialog.__init__(self, *args)
self.setupUi(self)
self.config = JSONConfig('store/search')
# We keep a cache of store plugins and reference them by name.
self.store_plugins = istores
self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL)
# Check for results and hung threads.
self.checker = QTimer()
self.hang_check = 0
self.model = Matches()
self.results_view.setModel(self.model)
# Add check boxes for each store so the user
# can disable searching specific stores on a
# per search basis.
stores_group_layout = QVBoxLayout()
self.stores_group.setLayout(stores_group_layout)
for x in self.store_plugins:
cbox = QCheckBox(x)
cbox.setChecked(True)
stores_group_layout.addWidget(cbox)
setattr(self, 'store_check_' + x, cbox)
stores_group_layout.addStretch()
# Create and add the progress indicator
self.pi = ProgressIndicator(self, 24)
self.bottom_layout.insertWidget(0, self.pi)
self.search.clicked.connect(self.do_search)
self.checker.timeout.connect(self.get_results)
self.results_view.activated.connect(self.open_store)
self.select_all_stores.clicked.connect(self.stores_select_all)
self.select_invert_stores.clicked.connect(self.stores_select_invert)
self.select_none_stores.clicked.connect(self.stores_select_none)
self.finished.connect(self.dialog_closed)
self.restore_state()
def resize_columns(self):
total = 600
# Cover
self.results_view.setColumnWidth(0, 85)
total = total - 85
# Title
self.results_view.setColumnWidth(1,int(total*.35))
# Author
self.results_view.setColumnWidth(2,int(total*.35))
# Price
self.results_view.setColumnWidth(3, int(total*.5))
# DRM
self.results_view.setColumnWidth(4, int(total*.5))
# Store
self.results_view.setColumnWidth(5, int(total*.15))
# Formats
self.results_view.setColumnWidth(6, int(total*.5))
def do_search(self, checked=False):
# Stop all running threads.
self.checker.stop()
self.search_pool.abort()
# Clear the visible results.
self.results_view.model().clear_results()
# Don't start a search if there is nothing to search for.
query = unicode(self.search_edit.text())
if not query.strip():
return
# Give the query to the results model so it can do
# futher filtering.
self.results_view.model().set_query(query)
# Plugins are in alphebetic order. Randomize the
# order of plugin names. This way plugins closer
# to a don't have an unfair advantage over
# plugins further from a.
store_names = self.store_plugins.keys()
if not store_names:
return
# Remove all of our internal filtering logic from the query.
query = self.clean_query(query)
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], TIMEOUT)
if self.search_pool.has_tasks():
self.hang_check = 0
self.checker.start(100)
self.search_pool.start_threads()
self.pi.startAnimation()
def clean_query(self, query):
query = query.lower()
# Remove control modifiers.
query = query.replace('\\', '')
query = query.replace('!', '')
query = query.replace('=', '')
query = query.replace('~', '')
query = query.replace('>', '')
query = query.replace('<', '')
# Remove the prefix.
for loc in ( 'all', 'author', 'authors', 'title'):
query = re.sub(r'%s:"?(?P<a>[^\s"]+)"?' % loc, '\g<a>', query)
# Remove the prefix and search text.
for loc in ('cover', 'drm', 'format', 'formats', 'price', 'store'):
query = re.sub(r'%s:"[^"]"' % loc, '', query)
query = re.sub(r'%s:[^\s]*' % loc, '', query)
# Remove logic.
query = re.sub(r'(^|\s)(and|not|or)(\s|$)', ' ', query)
# Remove excess whitespace.
query = re.sub(r'\s{2,}', ' ', query)
query = query.strip()
return query
def save_state(self):
self.config['store_search_geometry'] = bytearray(self.saveGeometry())
self.config['store_search_store_splitter_state'] = bytearray(self.store_splitter.saveState())
self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
store_check = {}
for n in self.store_plugins:
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
self.config['store_search_store_checked'] = store_check
def restore_state(self):
geometry = self.config.get('store_search_geometry', None)
if geometry:
self.restoreGeometry(geometry)
splitter_state = self.config.get('store_search_store_splitter_state', None)
if splitter_state:
self.store_splitter.restoreState(splitter_state)
results_cwidth = self.config.get('store_search_results_view_column_width', None)
if results_cwidth:
for i, x in enumerate(results_cwidth):
if i >= self.model.columnCount():
break
self.results_view.setColumnWidth(i, x)
else:
self.resize_columns()
store_check = self.config.get('store_search_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])
def get_results(self):
# We only want the search plugins to run
# a maximum set amount of time before giving up.
self.hang_check += 1
if self.hang_check >= HANG_TIME:
self.search_pool.abort()
self.checker.stop()
self.pi.stopAnimation()
else:
# Stop the checker if not threads are running.
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
self.checker.stop()
self.pi.stopAnimation()
while self.search_pool.has_results():
res, store_plugin = self.search_pool.get_result()
if res:
self.results_view.model().add_result(res, store_plugin)
def open_store(self, index):
result = self.results_view.model().get_result(index)
self.store_plugins[result.store_name].open(self, result.detail_item)
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():
check.setChecked(True)
def stores_select_invert(self):
for check in self.get_store_checks():
check.setChecked(not check.isChecked())
def stores_select_none(self):
for check in self.get_store_checks():
check.setChecked(False)
def dialog_closed(self, result):
self.model.closing()
self.search_pool.abort()
self.save_state()
class GenericDownloadThreadPool(object):
'''
add_task must be implemented in a subclass.
'''
def __init__(self, thread_type, thread_count):
self.thread_type = thread_type
self.thread_count = thread_count
self.tasks = Queue()
self.results = Queue()
self.threads = []
def add_task(self):
raise NotImplementedError()
def start_threads(self):
for i in range(self.thread_count):
t = self.thread_type(self.tasks, self.results)
self.threads.append(t)
t.start()
def abort(self):
self.tasks = Queue()
self.results = Queue()
for t in self.threads:
t.abort()
self.threads = []
def has_tasks(self):
return not self.tasks.empty()
def get_result(self):
return self.results.get()
def get_result_no_wait(self):
return self.results.get_nowait()
def result_count(self):
return len(self.results)
def has_results(self):
return not self.results.empty()
def threads_running(self):
for t in self.threads:
if t.is_alive():
return True
return False
class SearchThreadPool(GenericDownloadThreadPool):
'''
Threads will run until there is no work or
abort is called. Create and start new threads
using start_threads(). Reset by calling abort().
Example:
sp = SearchThreadPool(SearchThread, 3)
add tasks using add_task(...)
sp.start_threads()
all threads have finished.
sp.abort()
add tasks using add_task(...)
sp.start_threads()
'''
def add_task(self, query, store_name, store_plugin, timeout):
self.tasks.put((query, store_name, store_plugin, timeout))
class SearchThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
def abort(self):
self._run = False
def run(self):
while self._run and not self.tasks.empty():
try:
query, store_name, store_plugin, timeout = self.tasks.get()
for res in store_plugin.search(query, timeout=timeout):
if not self._run:
return
res.store_name = store_name
self.results.put((res, store_plugin))
self.tasks.task_done()
except:
traceback.print_exc()
class CoverThreadPool(GenericDownloadThreadPool):
'''
Once started all threads run until abort is called.
'''
def add_task(self, search_result, update_callback, timeout=5):
self.tasks.put((search_result, update_callback, timeout))
class CoverThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
self.br = browser()
def abort(self):
self._run = False
def run(self):
while self._run:
try:
time.sleep(.1)
while not self.tasks.empty():
if not self._run:
break
result, callback, timeout = self.tasks.get()
if result and result.cover_url:
with closing(self.br.open(result.cover_url, timeout=timeout)) as f:
result.cover_data = f.read()
result.cover_data = thumbnail(result.cover_data, 64, 64)[2]
callback()
self.tasks.task_done()
except:
continue
class DetailsThreadPool(GenericDownloadThreadPool):
'''
Once started all threads run until abort is called.
'''
def add_task(self, search_result, store_plugin, update_callback, timeout=10):
self.tasks.put((search_result, store_plugin, update_callback, timeout))
class DetailsThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
def abort(self):
self._run = False
def run(self):
while self._run:
try:
time.sleep(.1)
while not self.tasks.empty():
if not self._run:
break
result, store_plugin, callback, timeout = self.tasks.get()
if result:
store_plugin.get_details(result, timeout)
callback(result)
self.tasks.task_done()
except:
continue
class Matches(QAbstractItemModel):
HEADERS = [_('Cover'), _('Title'), _('Author(s)'), _('Price'), _('DRM'), _('Store'), _('Formats')]
def __init__(self):
QAbstractItemModel.__init__(self)
self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight(64,
Qt.SmoothTransformation)
self.DRM_UNLOCKED_ICON = QPixmap(I('drm-unlocked.png')).scaledToHeight(64,
Qt.SmoothTransformation)
self.DRM_UNKNOWN_ICON = QPixmap(I('dialog_question.png')).scaledToHeight(64,
Qt.SmoothTransformation)
# All matches. Used to determine the order to display
# self.matches because the SearchFilter returns
# matches unordered.
self.all_matches = []
# Only the showing matches.
self.matches = []
self.query = ''
self.search_filter = SearchFilter()
self.cover_pool = CoverThreadPool(CoverThread, 2)
self.cover_pool.start_threads()
self.details_pool = DetailsThreadPool(DetailsThread, 4)
self.details_pool.start_threads()
def closing(self):
self.cover_pool.abort()
self.details_pool.abort()
def clear_results(self):
self.all_matches = []
self.matches = []
self.all_matches = []
self.search_filter.clear_search_results()
self.query = ''
self.cover_pool.abort()
self.cover_pool.start_threads()
self.details_pool.abort()
self.details_pool.start_threads()
self.reset()
def add_result(self, result, store_plugin):
if result not in self.all_matches:
self.layoutAboutToBeChanged.emit()
self.all_matches.append(result)
self.search_filter.add_search_result(result)
if result.cover_url:
result.cover_queued = True
self.cover_pool.add_task(result, self.filter_results)
else:
result.cover_queued = False
self.details_pool.add_task(result, store_plugin, self.got_result_details)
self.filter_results()
self.layoutChanged.emit()
def get_result(self, index):
row = index.row()
if row < len(self.matches):
return self.matches[row]
else:
return None
def filter_results(self):
self.layoutAboutToBeChanged.emit()
if self.query:
self.matches = list(self.search_filter.parse(self.query))
else:
self.matches = list(self.search_filter.universal_set())
self.reorder_matches()
self.layoutChanged.emit()
def got_result_details(self, result):
if not result.cover_queued and result.cover_url:
result.cover_queued = True
self.cover_pool.add_task(result, self.filter_results)
if result in self.matches:
row = self.matches.index(result)
self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount() - 1))
if result.drm not in (SearchResult.DRM_LOCKED, SearchResult.DRM_UNLOCKED, SearchResult.DRM_UNKNOWN):
result.drm = SearchResult.DRM_UNKNOWN
self.filter_results()
def set_query(self, query):
self.query = query
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 == Qt.DisplayRole:
if col == 1:
return QVariant(result.title)
elif col == 2:
return QVariant(result.author)
elif col == 3:
return QVariant(result.price)
elif col == 5:
return QVariant(result.store_name)
elif col == 6:
return QVariant(result.formats)
return NONE
elif role == Qt.DecorationRole:
if col == 0 and result.cover_data:
p = QPixmap()
p.loadFromData(result.cover_data)
return QVariant(p)
if col == 4:
if result.drm == SearchResult.DRM_LOCKED:
return QVariant(self.DRM_LOCKED_ICON)
elif result.drm == SearchResult.DRM_UNLOCKED:
return QVariant(self.DRM_UNLOCKED_ICON)
elif result.drm == SearchResult.DRM_UNKNOWN:
return QVariant(self.DRM_UNKNOWN_ICON)
elif role == Qt.ToolTipRole:
if col == 1:
return QVariant('<p>%s</p>' % result.title)
elif col == 2:
return QVariant('<p>%s</p>' % result.author)
elif col == 3:
return QVariant('<p>' + _('Detected price as: %s. Check with the store before making a purchase to verify this price is correct. This price often does not include promotions the store may be running.') % result.price + '</p>')
elif col == 4:
if result.drm == SearchResult.DRM_LOCKED:
return QVariant('<p>' + _('This book as been detected as having DRM restrictions. This book may not work with your reader and you will have limitations placed upon you as to what you can do with this book. Check with the store before making any purchases to ensure you can actually read this book.') + '</p>')
elif result.drm == SearchResult.DRM_UNLOCKED:
return QVariant('<p>' + _('This book has been detected as being DRM Free. You should be able to use this book on any device provided it is in a format calibre supports for conversion. However, before making a purchase double check the DRM status with the store. The store may not be disclosing the use of DRM.') + '</p>')
else:
return QVariant('<p>' + _('The DRM status of this book could not be determined. There is a very high likelihood that this book is actually DRM restricted.') + '</p>')
elif col == 5:
return QVariant('<p>%s</p>' % result.store_name)
elif col == 6:
return QVariant('<p>%s</p>' % result.formats)
elif role == Qt.SizeHintRole:
return QSize(64, 64)
return NONE
def data_as_text(self, result, col):
text = ''
if col == 1:
text = result.title
elif col == 2:
text = result.author
elif col == 3:
text = comparable_price(result.price)
elif col == 4:
if result.drm == SearchResult.DRM_UNLOCKED:
text = 'a'
elif result.drm == SearchResult.DRM_LOCKED:
text = 'b'
else:
text = 'c'
elif col == 5:
text = result.store_name
elif col == 6:
text = ', '.join(sorted(result.formats.split(',')))
return text
def sort(self, col, order, reset=True):
if not self.matches:
return
descending = order == Qt.DescendingOrder
self.all_matches.sort(None,
lambda x: sort_key(unicode(self.data_as_text(x, col))),
descending)
self.reorder_matches()
if reset:
self.reset()
def reorder_matches(self):
self.matches = sorted(self.matches, key=lambda x: self.all_matches.index(x))
class SearchFilter(SearchQueryParser):
USABLE_LOCATIONS = [
'all',
'author',
'authors',
'cover',
'drm',
'format',
'formats',
'price',
'title',
'store',
]
def __init__(self):
SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
self.srs = set([])
def add_search_result(self, search_result):
self.srs.add(search_result)
def clear_search_results(self):
self.srs = set([])
def universal_set(self):
return self.srs
def get_matches(self, location, query):
location = location.lower().strip()
if location == 'authors':
location = 'author'
elif 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 = {
'author': lambda x: x.author.lower(),
'cover': attrgetter('cover_url'),
'drm': attrgetter('drm'),
'format': attrgetter('formats'),
'price': lambda x: comparable_price(x.price),
'store': lambda x: x.store_name.lower(),
'title': lambda x: x.title.lower(),
}
for x in ('author', 'format'):
q[x+'s'] = q[x]
for sr in self.srs:
for locvalue in locations:
accessor = q[locvalue]
if query == 'true':
if locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_LOCKED:
matches.add(sr)
else:
if accessor(sr) is not None:
matches.add(sr)
continue
if query == 'false':
if locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_UNLOCKED:
matches.add(sr)
else:
if accessor(sr) is None:
matches.add(sr)
continue
# this is bool, so can't match below
if locvalue == 'drm':
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 == 'author' 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
traceback.print_exc()
return matches

View File

@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import time
import traceback
from contextlib import closing
from threading import Thread
from Queue import Queue
from calibre import browser
from calibre.utils.magick.draw import thumbnail
class GenericDownloadThreadPool(object):
'''
add_task must be implemented in a subclass.
'''
def __init__(self, thread_type, thread_count):
self.thread_type = thread_type
self.thread_count = thread_count
self.tasks = Queue()
self.results = Queue()
self.threads = []
def add_task(self):
raise NotImplementedError()
def start_threads(self):
for i in range(self.thread_count):
t = self.thread_type(self.tasks, self.results)
self.threads.append(t)
t.start()
def abort(self):
self.tasks = Queue()
self.results = Queue()
for t in self.threads:
t.abort()
self.threads = []
def has_tasks(self):
return not self.tasks.empty()
def get_result(self):
return self.results.get()
def get_result_no_wait(self):
return self.results.get_nowait()
def result_count(self):
return len(self.results)
def has_results(self):
return not self.results.empty()
def threads_running(self):
for t in self.threads:
if t.is_alive():
return True
return False
class SearchThreadPool(GenericDownloadThreadPool):
'''
Threads will run until there is no work or
abort is called. Create and start new threads
using start_threads(). Reset by calling abort().
Example:
sp = SearchThreadPool(SearchThread, 3)
add tasks using add_task(...)
sp.start_threads()
all threads have finished.
sp.abort()
add tasks using add_task(...)
sp.start_threads()
'''
def add_task(self, query, store_name, store_plugin, timeout):
self.tasks.put((query, store_name, store_plugin, timeout))
class SearchThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
def abort(self):
self._run = False
def run(self):
while self._run and not self.tasks.empty():
try:
query, store_name, store_plugin, timeout = self.tasks.get()
for res in store_plugin.search(query, timeout=timeout):
if not self._run:
return
res.store_name = store_name
self.results.put((res, store_plugin))
self.tasks.task_done()
except:
traceback.print_exc()
class CoverThreadPool(GenericDownloadThreadPool):
'''
Once started all threads run until abort is called.
'''
def add_task(self, search_result, update_callback, timeout=5):
self.tasks.put((search_result, update_callback, timeout))
class CoverThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
self.br = browser()
def abort(self):
self._run = False
def run(self):
while self._run:
try:
time.sleep(.1)
while not self.tasks.empty():
if not self._run:
break
result, callback, timeout = self.tasks.get()
if result and result.cover_url:
with closing(self.br.open(result.cover_url, timeout=timeout)) as f:
result.cover_data = f.read()
result.cover_data = thumbnail(result.cover_data, 64, 64)[2]
callback()
self.tasks.task_done()
except:
continue
class DetailsThreadPool(GenericDownloadThreadPool):
'''
Once started all threads run until abort is called.
'''
def add_task(self, search_result, store_plugin, update_callback, timeout=10):
self.tasks.put((search_result, store_plugin, update_callback, timeout))
class DetailsThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
def abort(self):
self._run = False
def run(self):
while self._run:
try:
time.sleep(.1)
while not self.tasks.empty():
if not self._run:
break
result, store_plugin, callback, timeout = self.tasks.get()
if result:
store_plugin.get_details(result, timeout)
callback(result)
self.tasks.task_done()
except:
continue

View File

@ -0,0 +1,331 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import re
from operator import attrgetter
from PyQt4.Qt import (Qt, QAbstractItemModel, QVariant, QPixmap, QModelIndex, QSize)
from calibre.gui2 import NONE
from calibre.gui2.store.search_result import SearchResult
from calibre.gui2.store.search.download_thread import DetailsThreadPool, \
DetailsThread, CoverThreadPool, CoverThread
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
def comparable_price(text):
if len(text) < 3 or text[-3] not in ('.', ','):
text += '00'
text = re.sub(r'\D', '', text)
text = text.rjust(6, '0')
return text
class Matches(QAbstractItemModel):
HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store')]
HTML_COLS = (1, 4)
def __init__(self):
QAbstractItemModel.__init__(self)
self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight(64,
Qt.SmoothTransformation)
self.DRM_UNLOCKED_ICON = QPixmap(I('drm-unlocked.png')).scaledToHeight(64,
Qt.SmoothTransformation)
self.DRM_UNKNOWN_ICON = QPixmap(I('dialog_question.png')).scaledToHeight(64,
Qt.SmoothTransformation)
# All matches. Used to determine the order to display
# self.matches because the SearchFilter returns
# matches unordered.
self.all_matches = []
# Only the showing matches.
self.matches = []
self.query = ''
self.search_filter = SearchFilter()
self.cover_pool = CoverThreadPool(CoverThread, 2)
self.cover_pool.start_threads()
self.details_pool = DetailsThreadPool(DetailsThread, 4)
self.details_pool.start_threads()
self.sort_col = 2
self.sort_order = Qt.AscendingOrder
def closing(self):
self.cover_pool.abort()
self.details_pool.abort()
def clear_results(self):
self.all_matches = []
self.matches = []
self.all_matches = []
self.search_filter.clear_search_results()
self.query = ''
self.cover_pool.abort()
self.cover_pool.start_threads()
self.details_pool.abort()
self.details_pool.start_threads()
self.reset()
def add_result(self, result, store_plugin):
if result not in self.all_matches:
self.layoutAboutToBeChanged.emit()
self.all_matches.append(result)
self.search_filter.add_search_result(result)
if result.cover_url:
result.cover_queued = True
self.cover_pool.add_task(result, self.filter_results)
else:
result.cover_queued = False
self.details_pool.add_task(result, store_plugin, self.got_result_details)
self.filter_results()
self.layoutChanged.emit()
def get_result(self, index):
row = index.row()
if row < len(self.matches):
return self.matches[row]
else:
return None
def has_results(self):
return len(self.matches) > 0
def filter_results(self):
self.layoutAboutToBeChanged.emit()
if self.query:
self.matches = list(self.search_filter.parse(self.query))
else:
self.matches = list(self.search_filter.universal_set())
self.sort(self.sort_col, self.sort_order, False)
self.layoutChanged.emit()
def got_result_details(self, result):
if not result.cover_queued and result.cover_url:
result.cover_queued = True
self.cover_pool.add_task(result, self.filter_results)
if result in self.matches:
row = self.matches.index(result)
self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount() - 1))
if result.drm not in (SearchResult.DRM_LOCKED, SearchResult.DRM_UNLOCKED, SearchResult.DRM_UNKNOWN):
result.drm = SearchResult.DRM_UNKNOWN
self.filter_results()
def set_query(self, query):
self.query = query
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 == Qt.DisplayRole:
if col == 1:
t = result.title if result.title else _('Unknown')
a = result.author if result.author else ''
return QVariant('<b>%s</b><br><i>%s</i>' % (t, a))
elif col == 2:
return QVariant(result.price)
elif col == 4:
return QVariant('%s<br>%s' % (result.store_name, result.formats))
return NONE
elif role == Qt.DecorationRole:
if col == 0 and result.cover_data:
p = QPixmap()
p.loadFromData(result.cover_data)
return QVariant(p)
if col == 3:
if result.drm == SearchResult.DRM_LOCKED:
return QVariant(self.DRM_LOCKED_ICON)
elif result.drm == SearchResult.DRM_UNLOCKED:
return QVariant(self.DRM_UNLOCKED_ICON)
elif result.drm == SearchResult.DRM_UNKNOWN:
return QVariant(self.DRM_UNKNOWN_ICON)
elif role == Qt.ToolTipRole:
if col == 1:
return QVariant('<p>%s</p>' % result.title)
elif col == 2:
return QVariant('<p>' + _('Detected price as: %s. Check with the store before making a purchase to verify this price is correct. This price often does not include promotions the store may be running.') % result.price + '</p>')
elif col == 3:
if result.drm == SearchResult.DRM_LOCKED:
return QVariant('<p>' + _('This book as been detected as having DRM restrictions. This book may not work with your reader and you will have limitations placed upon you as to what you can do with this book. Check with the store before making any purchases to ensure you can actually read this book.') + '</p>')
elif result.drm == SearchResult.DRM_UNLOCKED:
return QVariant('<p>' + _('This book has been detected as being DRM Free. You should be able to use this book on any device provided it is in a format calibre supports for conversion. However, before making a purchase double check the DRM status with the store. The store may not be disclosing the use of DRM.') + '</p>')
else:
return QVariant('<p>' + _('The DRM status of this book could not be determined. There is a very high likelihood that this book is actually DRM restricted.') + '</p>')
elif col == 4:
return QVariant('<p>%s</p>' % result.formats)
elif role == Qt.SizeHintRole:
return QSize(64, 64)
return NONE
def data_as_text(self, result, col):
text = ''
if col == 1:
text = result.title
elif col == 2:
text = comparable_price(result.price)
elif col == 3:
if result.drm == SearchResult.DRM_UNLOCKED:
text = 'a'
if result.drm == SearchResult.DRM_LOCKED:
text = 'b'
else:
text = 'c'
elif col == 4:
text = result.store_name
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.all_matches.sort(None,
lambda x: sort_key(unicode(self.data_as_text(x, col))),
descending)
self.reorder_matches()
if reset:
self.reset()
def reorder_matches(self):
self.matches = sorted(self.matches, key=lambda x: self.all_matches.index(x))
class SearchFilter(SearchQueryParser):
USABLE_LOCATIONS = [
'all',
'author',
'authors',
'cover',
'drm',
'format',
'formats',
'price',
'title',
'store',
]
def __init__(self):
SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
self.srs = set([])
def add_search_result(self, search_result):
self.srs.add(search_result)
def clear_search_results(self):
self.srs = set([])
def universal_set(self):
return self.srs
def get_matches(self, location, query):
location = location.lower().strip()
if location == 'authors':
location = 'author'
elif 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 = {
'author': lambda x: x.author.lower(),
'cover': attrgetter('cover_url'),
'drm': attrgetter('drm'),
'format': attrgetter('formats'),
'price': lambda x: comparable_price(x.price),
'store': lambda x: x.store_name.lower(),
'title': lambda x: x.title.lower(),
}
for x in ('author', 'format'):
q[x+'s'] = q[x]
for sr in self.srs:
for locvalue in locations:
accessor = q[locvalue]
if query == 'true':
if locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_LOCKED:
matches.add(sr)
else:
if accessor(sr) is not None:
matches.add(sr)
continue
if query == 'false':
if locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_UNLOCKED:
matches.add(sr)
else:
if accessor(sr) is None:
matches.add(sr)
continue
# this is bool, so can't match below
if locvalue == 'drm':
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 == 'author' 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
traceback.print_exc()
return matches

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import (QTreeView)
from calibre.gui2.metadata.single_download import RichTextDelegate
from calibre.gui2.store.search.models import Matches
class ResultsView(QTreeView):
def __init__(self, *args):
QTreeView.__init__(self,*args)
self._model = Matches()
self.setModel(self._model)
self.rt_delegate = RichTextDelegate(self)
for i in self._model.HTML_COLS:
self.setItemDelegateForColumn(i, self.rt_delegate)

View File

@ -0,0 +1,232 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import re
from random import shuffle
from PyQt4.Qt import (Qt, QDialog, QTimer, QCheckBox, QVBoxLayout)
from calibre.gui2 import JSONConfig, info_dialog
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.store.search.download_thread import SearchThreadPool, SearchThread
from calibre.gui2.store.search.search_ui import Ui_Dialog
HANG_TIME = 75000 # milliseconds seconds
TIMEOUT = 75 # seconds
SEARCH_THREAD_TOTAL = 4
COVER_DOWNLOAD_THREAD_TOTAL = 2
class SearchDialog(QDialog, Ui_Dialog):
def __init__(self, istores, *args):
QDialog.__init__(self, *args)
self.setupUi(self)
self.config = JSONConfig('store/search')
# We keep a cache of store plugins and reference them by name.
self.store_plugins = istores
self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL)
# Check for results and hung threads.
self.checker = QTimer()
self.hang_check = 0
# Add check boxes for each store so the user
# can disable searching specific stores on a
# per search basis.
stores_group_layout = QVBoxLayout()
self.stores_group.setLayout(stores_group_layout)
for x in self.store_plugins:
cbox = QCheckBox(x)
cbox.setChecked(True)
stores_group_layout.addWidget(cbox)
setattr(self, 'store_check_' + x, cbox)
stores_group_layout.addStretch()
# Create and add the progress indicator
self.pi = ProgressIndicator(self, 24)
self.top_layout.addWidget(self.pi)
self.search.clicked.connect(self.do_search)
self.checker.timeout.connect(self.get_results)
self.results_view.activated.connect(self.open_store)
self.select_all_stores.clicked.connect(self.stores_select_all)
self.select_invert_stores.clicked.connect(self.stores_select_invert)
self.select_none_stores.clicked.connect(self.stores_select_none)
self.finished.connect(self.dialog_closed)
self.restore_state()
def resize_columns(self):
total = 600
# Cover
self.results_view.setColumnWidth(0, 85)
total = total - 85
# Title / Author
self.results_view.setColumnWidth(1,int(total*.40))
# Price
self.results_view.setColumnWidth(2,int(total*.20))
# DRM
self.results_view.setColumnWidth(3, int(total*.15))
# Store / Formats
self.results_view.setColumnWidth(4, int(total*.25))
def do_search(self, checked=False):
# Stop all running threads.
self.checker.stop()
self.search_pool.abort()
# Clear the visible results.
self.results_view.model().clear_results()
# Don't start a search if there is nothing to search for.
query = unicode(self.search_edit.text())
if not query.strip():
return
# Give the query to the results model so it can do
# futher filtering.
self.results_view.model().set_query(query)
# Plugins are in alphebetic order. Randomize the
# order of plugin names. This way plugins closer
# to a don't have an unfair advantage over
# plugins further from a.
store_names = self.store_plugins.keys()
if not store_names:
return
# Remove all of our internal filtering logic from the query.
query = self.clean_query(query)
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], TIMEOUT)
if self.search_pool.has_tasks():
self.hang_check = 0
self.checker.start(100)
self.search_pool.start_threads()
self.pi.startAnimation()
def clean_query(self, query):
query = query.lower()
# Remove control modifiers.
query = query.replace('\\', '')
query = query.replace('!', '')
query = query.replace('=', '')
query = query.replace('~', '')
query = query.replace('>', '')
query = query.replace('<', '')
# Remove the prefix.
for loc in ( 'all', 'author', 'authors', 'title'):
query = re.sub(r'%s:"?(?P<a>[^\s"]+)"?' % loc, '\g<a>', query)
# Remove the prefix and search text.
for loc in ('cover', 'drm', 'format', 'formats', 'price', 'store'):
query = re.sub(r'%s:"[^"]"' % loc, '', query)
query = re.sub(r'%s:[^\s]*' % loc, '', query)
# Remove logic.
query = re.sub(r'(^|\s)(and|not|or)(\s|$)', ' ', query)
# Remove excess whitespace.
query = re.sub(r'\s{2,}', ' ', query)
query = query.strip()
return query
def save_state(self):
self.config['geometry'] = bytearray(self.saveGeometry())
self.config['store_splitter_state'] = bytearray(self.store_splitter.saveState())
self.config['results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())]
self.config['sort_col'] = self.results_view.model().sort_col
self.config['sort_order'] = self.results_view.model().sort_order
store_check = {}
for n in self.store_plugins:
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
self.config['store_checked'] = store_check
def restore_state(self):
geometry = self.config.get('geometry', None)
if geometry:
self.restoreGeometry(geometry)
splitter_state = self.config.get('store_splitter_state', None)
if splitter_state:
self.store_splitter.restoreState(splitter_state)
results_cwidth = self.config.get('results_view_column_width', None)
if results_cwidth:
for i, x in enumerate(results_cwidth):
if i >= self.results_view.model().columnCount():
break
self.results_view.setColumnWidth(i, x)
else:
self.resize_columns()
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])
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)
self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order)
def get_results(self):
# We only want the search plugins to run
# a maximum set amount of time before giving up.
self.hang_check += 1
if self.hang_check >= HANG_TIME:
self.search_pool.abort()
self.checker.stop()
self.pi.stopAnimation()
else:
# Stop the checker if not threads are running.
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
self.checker.stop()
self.pi.stopAnimation()
while self.search_pool.has_results():
res, store_plugin = self.search_pool.get_result()
if res:
self.results_view.model().add_result(res, store_plugin)
if not self.checker.isActive():
if not self.results_view.model().has_results():
info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False)
def open_store(self, index):
result = self.results_view.model().get_result(index)
self.store_plugins[result.store_name].open(self, result.detail_item)
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():
check.setChecked(True)
def stores_select_invert(self):
for check in self.get_store_checks():
check.setChecked(not check.isChecked())
def stores_select_none(self):
for check in self.get_store_checks():
check.setChecked(False)
def dialog_closed(self, result):
self.results_view.model().closing()
self.search_pool.abort()
self.save_state()

View File

@ -14,7 +14,7 @@
<string>Get Books</string> <string>Get Books</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset resource="../../../../resources/images.qrc"> <iconset>
<normaloff>:/images/store.png</normaloff>:/images/store.png</iconset> <normaloff>:/images/store.png</normaloff>:/images/store.png</iconset>
</property> </property>
<property name="sizeGripEnabled"> <property name="sizeGripEnabled">
@ -22,7 +22,7 @@
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="top_layout">
<item> <item>
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
@ -62,8 +62,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>170</width> <width>215</width>
<height>138</height> <height>116</height>
</rect> </rect>
</property> </property>
</widget> </widget>
@ -110,7 +110,7 @@
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<widget class="QTreeView" name="results_view"> <widget class="ResultsView" name="results_view">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch> <horstretch>1</horstretch>
@ -178,6 +178,13 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<customwidgets>
<customwidget>
<class>ResultsView</class>
<extends>QTreeView</extends>
<header>results_view.h</header>
</customwidget>
</customwidgets>
<resources> <resources>
<include location="../../../../resources/images.qrc"/> <include location="../../../../resources/images.qrc"/>
</resources> </resources>

View File

@ -25,7 +25,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class SmashwordsStore(BasicStoreConfig, StorePlugin): class SmashwordsStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.smashwords.com/' url = 'http://www.smashwords.com/'
aff_id = '?ref=usernone' aff_id = '?ref=usernone'
@ -38,12 +37,12 @@ class SmashwordsStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item + aff_id detail_url = url + detail_item + aff_id
url = url + aff_id url = url + aff_id
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else: else:
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):