mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
...
This commit is contained in:
commit
24ea3c4327
@ -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_()
|
||||||
|
|
||||||
|
@ -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):
|
||||||
'''
|
'''
|
||||||
|
@ -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
|
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
|
0
src/calibre/gui2/store/search/__init__.py
Normal file
0
src/calibre/gui2/store/search/__init__.py
Normal file
190
src/calibre/gui2/store/search/download_thread.py
Normal file
190
src/calibre/gui2/store/search/download_thread.py
Normal 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
|
331
src/calibre/gui2/store/search/models.py
Normal file
331
src/calibre/gui2/store/search/models.py
Normal 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
|
26
src/calibre/gui2/store/search/results_view.py
Normal file
26
src/calibre/gui2/store/search/results_view.py
Normal 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)
|
||||||
|
|
232
src/calibre/gui2/store/search/search.py
Normal file
232
src/calibre/gui2/store/search/search.py
Normal 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()
|
||||||
|
|
@ -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>
|
@ -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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user