Merge from trunk

This commit is contained in:
Charles Haley 2011-05-25 18:10:41 +01:00
commit 42d8b7ce1c
25 changed files with 1185 additions and 68 deletions

View File

@ -1163,7 +1163,7 @@ class StoreBNStore(StoreBase):
class StoreBeamEBooksDEStore(StoreBase): class StoreBeamEBooksDEStore(StoreBase):
name = 'Beam EBooks DE' name = 'Beam EBooks DE'
author = 'Charles Haley' author = 'Charles Haley'
description = u'Der eBook Shop.' description = u'Bei uns finden Sie: Tausende deutschsprachige eBooks; Alle eBooks ohne hartes DRM; PDF, ePub und Mobipocket Format; Sofortige Verfügbarkeit - 24 Stunden am Tag; Günstige Preise; eBooks für viele Lesegeräte, PC,Mac und Smartphones; Viele Gratis eBooks'
actual_plugin = 'calibre.gui2.store.beam_ebooks_de_plugin:BeamEBooksDEStore' actual_plugin = 'calibre.gui2.store.beam_ebooks_de_plugin:BeamEBooksDEStore'
drm_free_only = True drm_free_only = True
@ -1200,7 +1200,7 @@ class StoreEbookscomStore(StoreBase):
class StoreEPubBuyDEStore(StoreBase): class StoreEPubBuyDEStore(StoreBase):
name = 'EPUBBuy DE' name = 'EPUBBuy DE'
author = 'Charles Haley' author = 'Charles Haley'
description = u'Deutsch epub-Spezialisten.' description = u'Bei EPUBBuy.com finden Sie ausschliesslich eBooks im weitverbreiteten EPUB-Format und ohne DRM. So haben Sie die freie Wahl, wo Sie Ihr eBook lesen: Tablet, eBook-Reader, Smartphone oder einfach auf Ihrem PC. So macht eBook-Lesen Spaß!'
actual_plugin = 'calibre.gui2.store.epubbuy_de_plugin:EPubBuyDEStore' actual_plugin = 'calibre.gui2.store.epubbuy_de_plugin:EPubBuyDEStore'
drm_free_only = True drm_free_only = True
@ -1272,6 +1272,16 @@ class StoreKoboStore(StoreBase):
headquarters = 'CA' headquarters = 'CA'
formats = ['EPUB'] formats = ['EPUB']
class StoreLegimiStore(StoreBase):
name = 'Legimi'
author = u'Tomasz Długosz'
description = u'Tanie oraz darmowe ebooki, egazety i blogi w formacie EPUB, wprost na Twój e-czytnik, iPhone, iPad, Android i komputer'
actual_plugin = 'calibre.gui2.store.legimi_plugin:LegimiStore'
drm_free_only = False
headquarters = 'PL'
formats = ['EPUB']
class StoreManyBooksStore(StoreBase): class StoreManyBooksStore(StoreBase):
name = 'ManyBooks' name = 'ManyBooks'
description = u'Public domain and creative commons works from many sources.' description = u'Public domain and creative commons works from many sources.'
@ -1306,7 +1316,7 @@ class StoreOpenLibraryStore(StoreBase):
actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore' actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore'
drm_free_only = True drm_free_only = True
headquarters = ['US'] headquarters = 'US'
formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT'] formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT']
class StoreOReillyStore(StoreBase): class StoreOReillyStore(StoreBase):
@ -1371,7 +1381,7 @@ class StoreWoblinkStore(StoreBase):
actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore' actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore'
drm_free_only = False drm_free_only = False
location = 'PL' headquarters = 'PL'
formats = ['EPUB'] formats = ['EPUB']
plugins += [ plugins += [
@ -1393,6 +1403,7 @@ plugins += [
StoreGoogleBooksStore, StoreGoogleBooksStore,
StoreGutenbergStore, StoreGutenbergStore,
StoreKoboStore, StoreKoboStore,
StoreLegimiStore,
StoreManyBooksStore, StoreManyBooksStore,
StoreMobileReadStore, StoreMobileReadStore,
StoreNextoStore, StoreNextoStore,

View File

@ -59,7 +59,7 @@ class ANDROID(USBMS):
0x0489 : { 0xc001 : [0x0226], 0xc004 : [0x0226], }, 0x0489 : { 0xc001 : [0x0226], 0xc004 : [0x0226], },
# Acer # Acer
0x502 : { 0x3203 : [0x0100]}, 0x502 : { 0x3203 : [0x0100, 0x224]},
# Dell # Dell
0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]}, 0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]},

View File

@ -413,6 +413,13 @@ class EPUBOutput(OutputFormatPlugin):
rule.style.removeProperty('margin-left') rule.style.removeProperty('margin-left')
# padding-left breaks rendering in webkit and gecko # padding-left breaks rendering in webkit and gecko
rule.style.removeProperty('padding-left') rule.style.removeProperty('padding-left')
# Change whitespace:pre to pre-line to accommodate readers that
# cannot scroll horizontally
for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
style = rule.style
ws = style.getPropertyValue('white-space')
if ws == 'pre':
style.setProperty('white-space', 'pre-wrap')
# }}} # }}}

View File

@ -29,7 +29,7 @@ class Worker(Thread): # Get details {{{
Get book details from amazons book page in a separate thread Get book details from amazons book page in a separate thread
''' '''
def __init__(self, url, result_queue, browser, log, relevance, plugin, timeout=20): def __init__(self, url, result_queue, browser, log, relevance, domain, plugin, timeout=20):
Thread.__init__(self) Thread.__init__(self)
self.daemon = True self.daemon = True
self.url, self.result_queue = url, result_queue self.url, self.result_queue = url, result_queue
@ -37,7 +37,7 @@ class Worker(Thread): # Get details {{{
self.relevance, self.plugin = relevance, plugin self.relevance, self.plugin = relevance, plugin
self.browser = browser.clone_browser() self.browser = browser.clone_browser()
self.cover_url = self.amazon_id = self.isbn = None self.cover_url = self.amazon_id = self.isbn = None
self.domain = self.plugin.domain self.domain = domain
months = { months = {
'de': { 'de': {
@ -199,7 +199,8 @@ class Worker(Thread): # Get details {{{
return return
mi = Metadata(title, authors) mi = Metadata(title, authors)
mi.set_identifier('amazon', asin) idtype = 'amazon' if self.domain == 'com' else 'amazon_'+self.domain
mi.set_identifier(idtype, asin)
self.amazon_id = asin self.amazon_id = asin
try: try:
@ -404,12 +405,30 @@ class Amazon(Source):
'country\'s Amazon website.'), choices=AMAZON_DOMAINS), 'country\'s Amazon website.'), choices=AMAZON_DOMAINS),
) )
def get_domain_and_asin(self, identifiers):
for key, val in identifiers.iteritems():
key = key.lower()
if key in ('amazon', 'asin'):
return 'com', val
if key.startswith('amazon_'):
domain = key.split('_')[-1]
if domain and domain in self.AMAZON_DOMAINS:
return domain, val
return None, None
def get_book_url(self, identifiers): # {{{ def get_book_url(self, identifiers): # {{{
asin = identifiers.get('amazon', None) domain, asin = self.get_domain_and_asin(identifiers)
if asin is None: if domain and asin:
asin = identifiers.get('asin', None) url = None
if asin: if domain == 'com':
return ('amazon', asin, 'http://amzn.com/%s'%asin) url = 'http://amzn.com/'+asin
elif domain == 'uk':
url = 'http://www.amazon.co.uk/dp/'+asin
else:
url = 'http://www.amazon.%s/dp/%s'%(domain, asin)
if url:
idtype = 'amazon' if self.domain == 'com' else 'amazon_'+self.domain
return (idtype, asin, url)
# }}} # }}}
@property @property
@ -420,9 +439,15 @@ class Amazon(Source):
return domain return domain
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{ def create_query(self, log, title=None, authors=None, identifiers={}, # {{{
domain=None):
if domain is None:
domain = self.domain domain = self.domain
idomain, asin = self.get_domain_and_asin(identifiers)
if idomain is not None:
domain = idomain
# See the amazon detailed search page to get all options # See the amazon detailed search page to get all options
q = { 'search-alias' : 'aps', q = { 'search-alias' : 'aps',
'unfiltered' : '1', 'unfiltered' : '1',
@ -433,7 +458,6 @@ class Amazon(Source):
else: else:
q['sort'] = 'relevancerank' q['sort'] = 'relevancerank'
asin = identifiers.get('amazon', None)
isbn = check_isbn(identifiers.get('isbn', None)) isbn = check_isbn(identifiers.get('isbn', None))
if asin is not None: if asin is not None:
@ -456,23 +480,22 @@ class Amazon(Source):
if not ('field-keywords' in q or 'field-isbn' in q or if not ('field-keywords' in q or 'field-isbn' in q or
('field-title' in q)): ('field-title' in q)):
# Insufficient metadata to make an identify query # Insufficient metadata to make an identify query
return None return None, None
latin1q = dict([(x.encode('latin1', 'ignore'), y.encode('latin1', latin1q = dict([(x.encode('latin1', 'ignore'), y.encode('latin1',
'ignore')) for x, y in 'ignore')) for x, y in
q.iteritems()]) q.iteritems()])
udomain = domain
if domain == 'uk': if domain == 'uk':
domain = 'co.uk' udomain = 'co.uk'
url = 'http://www.amazon.%s/s/?'%domain + urlencode(latin1q) url = 'http://www.amazon.%s/s/?'%udomain + urlencode(latin1q)
return url return url, domain
# }}} # }}}
def get_cached_cover_url(self, identifiers): # {{{ def get_cached_cover_url(self, identifiers): # {{{
url = None url = None
asin = identifiers.get('amazon', None) domain, asin = self.get_domain_and_asin(identifiers)
if asin is None:
asin = identifiers.get('asin', None)
if asin is None: if asin is None:
isbn = identifiers.get('isbn', None) isbn = identifiers.get('isbn', None)
if isbn is not None: if isbn is not None:
@ -489,7 +512,7 @@ class Amazon(Source):
Note this method will retry without identifiers automatically if no Note this method will retry without identifiers automatically if no
match is found with identifiers. match is found with identifiers.
''' '''
query = self.create_query(log, title=title, authors=authors, query, domain = self.create_query(log, title=title, authors=authors,
identifiers=identifiers) identifiers=identifiers)
if query is None: if query is None:
log.error('Insufficient metadata to construct query') log.error('Insufficient metadata to construct query')
@ -571,7 +594,7 @@ class Amazon(Source):
log.error('No matches found with query: %r'%query) log.error('No matches found with query: %r'%query)
return return
workers = [Worker(url, result_queue, br, log, i, self) for i, url in workers = [Worker(url, result_queue, br, log, i, domain, self) for i, url in
enumerate(matches)] enumerate(matches)]
for w in workers: for w in workers:

View File

@ -211,7 +211,10 @@ class Douban(Source):
'q': q, 'q': q,
}) })
if self.DOUBAN_API_KEY and self.DOUBAN_API_KEY != '': if self.DOUBAN_API_KEY and self.DOUBAN_API_KEY != '':
if t == "isbn" or t == "subject":
url = url + "?apikey=" + self.DOUBAN_API_KEY url = url + "?apikey=" + self.DOUBAN_API_KEY
else:
url = url + "&apikey=" + self.DOUBAN_API_KEY
return url return url
# }}} # }}}

View File

@ -34,6 +34,8 @@ class StoreAction(InterfaceAction):
self.store_list_menu = self.store_menu.addMenu(_('Stores')) self.store_list_menu = self.store_menu.addMenu(_('Stores'))
for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()): for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()):
self.store_list_menu.addAction(n, partial(self.open_store, p)) self.store_list_menu.addAction(n, partial(self.open_store, p))
self.store_menu.addSeparator()
self.store_menu.addAction(_('Choose stores'), self.choose)
self.qaction.setMenu(self.store_menu) self.qaction.setMenu(self.store_menu)
def do_search(self): def do_search(self):
@ -107,6 +109,13 @@ class StoreAction(InterfaceAction):
query = 'author:"%s" title:"%s"' % (self._get_author(row), self._get_title(row)) query = 'author:"%s" title:"%s"' % (self._get_author(row), self._get_title(row))
self.search(query) self.search(query)
def choose(self):
from calibre.gui2.store.config.chooser.chooser_dialog import StoreChooserDialog
d = StoreChooserDialog(self.gui)
d.exec_()
self.gui.load_store_plugins()
self.load_menu()
def open_store(self, store_plugin): def open_store(self, store_plugin):
self.show_disclaimer() self.show_disclaimer()
store_plugin.open(self.gui) store_plugin.open(self.gui)

View File

@ -0,0 +1,131 @@
# -*- 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 PyQt4.Qt import (QDialog, QDialogButtonBox)
from calibre.gui2.store.config.chooser.adv_search_builder_ui import Ui_Dialog
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
def __init__(self, parent):
QDialog.__init__(self, parent)
self.setupUi(self)
self.buttonBox.accepted.connect(self.advanced_search_button_pushed)
self.tab_2_button_box.accepted.connect(self.accept)
self.tab_2_button_box.rejected.connect(self.reject)
self.clear_button.clicked.connect(self.clear_button_pushed)
self.adv_search_used = False
self.mc = ''
self.tabWidget.setCurrentIndex(0)
self.tabWidget.currentChanged[int].connect(self.tab_changed)
self.tab_changed(0)
def tab_changed(self, idx):
if idx == 1:
self.tab_2_button_box.button(QDialogButtonBox.Ok).setDefault(True)
else:
self.buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
def advanced_search_button_pushed(self):
self.adv_search_used = True
self.accept()
def clear_button_pushed(self):
self.name_box.setText('')
self.description_box.setText('')
self.headquarters_box.setText('')
self.format_box.setText('')
self.enabled_combo.setIndex(0)
self.drm_combo.setIndex(0)
def tokens(self, raw):
phrases = re.findall(r'\s*".*?"\s*', raw)
for f in phrases:
raw = raw.replace(f, ' ')
phrases = [t.strip('" ') for t in phrases]
return ['"' + self.mc + t + '"' for t in phrases + [r.strip() for r in raw.split()]]
def search_string(self):
if self.adv_search_used:
return self.adv_search_string()
else:
return self.box_search_string()
def adv_search_string(self):
mk = self.matchkind.currentIndex()
if mk == CONTAINS_MATCH:
self.mc = ''
elif mk == EQUALS_MATCH:
self.mc = '='
else:
self.mc = '~'
all, any, phrase, none = map(lambda x: unicode(x.text()),
(self.all, self.any, self.phrase, self.none))
all, any, none = map(self.tokens, (all, any, none))
phrase = phrase.strip()
all = ' and '.join(all)
any = ' or '.join(any)
none = ' and not '.join(none)
ans = ''
if phrase:
ans += '"%s"'%phrase
if all:
ans += (' and ' if ans else '') + all
if none:
ans += (' and not ' if ans else 'not ') + none
if any:
ans += (' or ' if ans else '') + any
return ans
def token(self):
txt = unicode(self.text.text()).strip()
if txt:
if self.negate.isChecked():
txt = '!'+txt
tok = self.FIELDS[unicode(self.field.currentText())]+txt
if re.search(r'\s', tok):
tok = '"%s"'%tok
return tok
def box_search_string(self):
mk = self.matchkind.currentIndex()
if mk == CONTAINS_MATCH:
self.mc = ''
elif mk == EQUALS_MATCH:
self.mc = '='
else:
self.mc = '~'
ans = []
self.box_last_values = {}
name = unicode(self.name_box.text()).strip()
if name:
ans.append('name:"' + self.mc + name + '"')
description = unicode(self.description_box.text()).strip()
if description:
ans.append('description:"' + self.mc + description + '"')
headquarters = unicode(self.headquarters_box.text()).strip()
if headquarters:
ans.append('headquarters:"' + self.mc + headquarters + '"')
format = unicode(self.format_box.text()).strip()
if format:
ans.append('format:"' + self.mc + format + '"')
enabled = unicode(self.enabled_combo.currentText()).strip()
if enabled:
ans.append('enabled:' + enabled)
drm = unicode(self.drm_combo.currentText()).strip()
if drm:
ans.append('drm:' + drm)
if ans:
return ' and '.join(ans)
return ''

View File

@ -0,0 +1,416 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>752</width>
<height>472</height>
</rect>
</property>
<property name="windowTitle">
<string>Advanced Search</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/images/search.png</normaloff>:/images/search.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&amp;What kind of match to use:</string>
</property>
<property name="buddy">
<cstring>matchkind</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="matchkind">
<item>
<property name="text">
<string>Contains: the word or phrase matches anywhere in the metadata field</string>
</property>
</item>
<item>
<property name="text">
<string>Equals: the word or phrase must match the entire metadata field</string>
</property>
</item>
<item>
<property name="text">
<string>Regular expression: the expression must match anywhere in the metadata field</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>A&amp;dvanced Search</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Find entries that have...</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;All these words:</string>
</property>
<property name="buddy">
<cstring>all</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="all"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>This exact &amp;phrase:</string>
</property>
<property name="buddy">
<cstring>all</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="phrase"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>&amp;One or more of these words:</string>
</property>
<property name="buddy">
<cstring>all</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="any"/>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>But dont show entries that have...</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Any of these &amp;unwanted words:</string>
</property>
<property name="buddy">
<cstring>all</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="none"/>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>See the &lt;a href=&quot;http://calibre-ebook.com/user_manual/gui.html#the-search-interface&quot;&gt;User Manual&lt;/a&gt; for more help</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Nam&amp;e/Description ...</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>&amp;Name:</string>
</property>
<property name="buddy">
<cstring>name_box</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="EnLineEdit" name="name_box">
<property name="toolTip">
<string>Enter the title.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>&amp;Description:</string>
</property>
<property name="buddy">
<cstring>description_box</cstring>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="price_label">
<property name="text">
<string>&amp;Headquarters:</string>
</property>
<property name="buddy">
<cstring>headquarters_box</cstring>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QPushButton" name="clear_button">
<property name="text">
<string>&amp;Clear</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="tab_2_button_box">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Search only in specific fields:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="EnLineEdit" name="description_box"/>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="format_box"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>&amp;Format:</string>
</property>
<property name="buddy">
<cstring>format_box</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="EnLineEdit" name="headquarters_box"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Enabled:</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>DRM:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="enabled_combo">
<item>
<property name="text">
<string/>
</property>
</item>
<item>
<property name="text">
<string>true</string>
</property>
</item>
<item>
<property name="text">
<string>false</string>
</property>
</item>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="drm_combo">
<item>
<property name="text">
<string/>
</property>
</item>
<item>
<property name="text">
<string>true</string>
</property>
</item>
<item>
<property name="text">
<string>false</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="1" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>EnLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>all</tabstop>
<tabstop>phrase</tabstop>
<tabstop>any</tabstop>
<tabstop>none</tabstop>
<tabstop>buttonBox</tabstop>
<tabstop>name_box</tabstop>
<tabstop>description_box</tabstop>
<tabstop>headquarters_box</tabstop>
<tabstop>format_box</tabstop>
<tabstop>clear_button</tabstop>
<tabstop>tab_2_button_box</tabstop>
<tabstop>tabWidget</tabstop>
<tabstop>matchkind</tabstop>
</tabstops>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,28 @@
# -*- 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 (QDialog, QDialogButtonBox, QVBoxLayout)
from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget
class StoreChooserDialog(QDialog):
def __init__(self, parent):
QDialog.__init__(self, parent)
self.setWindowTitle(_('Choose stores'))
button_box = QDialogButtonBox(QDialogButtonBox.Close)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
v = QVBoxLayout(self)
self.config_widget = StoreChooserWidget()
v.addWidget(self.config_widget)
v.addWidget(button_box)
self.resize(800, 600)

View File

@ -0,0 +1,35 @@
# -*- 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 (QWidget, QIcon, QDialog)
from calibre.gui2.store.config.chooser.adv_search_builder import AdvSearchBuilderDialog
from calibre.gui2.store.config.chooser.chooser_widget_ui import Ui_Form
class StoreChooserWidget(QWidget, Ui_Form):
def __init__(self):
QWidget.__init__(self)
self.setupUi(self)
self.adv_search_builder.setIcon(QIcon(I('search.png')))
self.search.clicked.connect(self.do_search)
self.adv_search_builder.clicked.connect(self.build_adv_search)
self.results_view.activated.connect(self.toggle_plugin)
def do_search(self):
self.results_view.model().search(unicode(self.query.text()))
def toggle_plugin(self, index):
self.results_view.model().toggle_plugin(index)
def build_adv_search(self):
adv = AdvSearchBuilderDialog(self)
if adv.exec_() == QDialog.Accepted:
self.query.setText(adv.search_string())

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>610</width>
<height>553</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Query:</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="adv_search_builder">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="query"/>
</item>
<item>
<widget class="QPushButton" name="search">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="ResultsView" name="results_view">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ResultsView</class>
<extends>QTreeView</extends>
<header>results_view.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,244 @@
# -*- 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 (Qt, QAbstractItemModel, QIcon, QVariant, QModelIndex)
from calibre.gui2 import NONE
from calibre.customize.ui import is_disabled, disable_plugin, enable_plugin
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH
from calibre.utils.icu import sort_key
from calibre.utils.search_query_parser import SearchQueryParser
class Matches(QAbstractItemModel):
HEADERS = [_('Enabled'), _('Name'), _('No DRM'), _('Headquarters'), _('Formats')]
HTML_COLS = [1]
def __init__(self, plugins):
QAbstractItemModel.__init__(self)
self.NO_DRM_ICON = QIcon(I('ok.png'))
self.all_matches = plugins
self.matches = plugins
self.filter = ''
self.search_filter = SearchFilter(self.all_matches)
self.sort_col = 1
self.sort_order = Qt.AscendingOrder
def get_plugin(self, index):
row = index.row()
if row < len(self.matches):
return self.matches[row]
else:
return None
def search(self, filter):
self.filter = filter.strip()
if not self.filter:
self.matches = self.all_matches
else:
try:
self.matches = list(self.search_filter.parse(self.filter))
except:
self.matches = self.all_matches
self.layoutChanged.emit()
self.sort(self.sort_col, self.sort_order)
def toggle_plugin(self, index):
new_index = self.createIndex(index.row(), 0)
data = QVariant(is_disabled(self.get_plugin(index)))
self.setData(new_index, data, Qt.CheckStateRole)
def index(self, row, column, parent=QModelIndex()):
return self.createIndex(row, column)
def parent(self, index):
if not index.isValid() or index.internalId() == 0:
return QModelIndex()
return self.createIndex(0, 0)
def rowCount(self, *args):
return len(self.matches)
def columnCount(self, *args):
return len(self.HEADERS)
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ''
if orientation == Qt.Horizontal:
if section < len(self.HEADERS):
text = self.HEADERS[section]
return QVariant(text)
else:
return QVariant(section+1)
def data(self, index, role):
row, col = index.row(), index.column()
result = self.matches[row]
if role in (Qt.DisplayRole, Qt.EditRole):
if col == 1:
return QVariant('<b>%s</b><br><i>%s</i>' % (result.name, result.description))
elif col == 3:
return QVariant(result.headquarters)
elif col == 4:
return QVariant(', '.join(result.formats).upper())
elif role == Qt.DecorationRole:
if col == 2:
if result.drm_free_only:
return QVariant(self.NO_DRM_ICON)
elif role == Qt.CheckStateRole:
if col == 0:
if is_disabled(result):
return Qt.Unchecked
return Qt.Checked
elif role == Qt.ToolTipRole:
return QVariant('<p>%s</p>' % result.description)
return NONE
def setData(self, index, data, role):
if not index.isValid():
return False
row, col = index.row(), index.column()
if col == 0:
if data.toBool():
enable_plugin(self.get_plugin(index))
else:
disable_plugin(self.get_plugin(index))
self.dataChanged.emit(self.index(index.row(), 0), self.index(index.row(), self.columnCount() - 1))
return True
def flags(self, index):
if index.column() == 0:
return QAbstractItemModel.flags(self, index) | Qt.ItemIsUserCheckable
return QAbstractItemModel.flags(self, index)
def data_as_text(self, match, col):
text = ''
if col == 0:
text = 'b' if is_disabled(match) else 'a'
elif col == 1:
text = match.name
elif col == 2:
text = 'b' if getattr(match, 'drm', True) else 'a'
elif col == 3:
text = getattr(match, 'headquarters', '')
return text
def sort(self, col, order, reset=True):
self.sort_col = col
self.sort_order = order
if not self.matches:
return
descending = order == Qt.DescendingOrder
self.matches.sort(None,
lambda x: sort_key(unicode(self.data_as_text(x, col))),
descending)
if reset:
self.reset()
class SearchFilter(SearchQueryParser):
USABLE_LOCATIONS = [
'all',
'description',
'drm',
'enabled',
'format',
'formats',
'headquarters',
'name',
]
def __init__(self, all_plugins=[]):
SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
self.srs = set(all_plugins)
def universal_set(self):
return self.srs
def get_matches(self, location, query):
location = location.lower().strip()
if location == 'formats':
location = 'format'
matchkind = CONTAINS_MATCH
if len(query) > 1:
if query.startswith('\\'):
query = query[1:]
elif query.startswith('='):
matchkind = EQUALS_MATCH
query = query[1:]
elif query.startswith('~'):
matchkind = REGEXP_MATCH
query = query[1:]
if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D
query = query.lower()
if location not in self.USABLE_LOCATIONS:
return set([])
matches = set([])
all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
locations = all_locs if location == 'all' else [location]
q = {
'description': lambda x: x.description.lower(),
'drm': lambda x: not x.drm_free_only,
'enabled': lambda x: not is_disabled(x),
'format': lambda x: ','.join(x.formats).lower(),
'headquarters': lambda x: x.headquarters.lower(),
'name': lambda x : x.name.lower(),
}
q['formats'] = q['format']
for sr in self.srs:
for locvalue in locations:
accessor = q[locvalue]
if query == 'true':
if locvalue in ('drm', 'enabled'):
if accessor(sr) == True:
matches.add(sr)
elif accessor(sr) is not None:
matches.add(sr)
continue
if query == 'false':
if locvalue in ('drm', 'enabled'):
if accessor(sr) == False:
matches.add(sr)
elif accessor(sr) is None:
matches.add(sr)
continue
# this is bool, so can't match below
if locvalue in ('drm', 'enabled'):
continue
try:
### Can't separate authors because comma is used for name sep and author sep
### Exact match might not get what you want. For that reason, turn author
### exactmatch searches into contains searches.
if locvalue == 'name' and matchkind == EQUALS_MATCH:
m = CONTAINS_MATCH
else:
m = matchkind
if locvalue == 'format':
vals = accessor(sr).split(',')
else:
vals = [accessor(sr)]
if _match(query, vals, m):
matches.add(sr)
break
except ValueError: # Unicode errors
import traceback
traceback.print_exc()
return matches

View File

@ -0,0 +1,34 @@
# -*- 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 (Qt, QTreeView, QSize)
from calibre.customize.ui import store_plugins
from calibre.gui2.metadata.single_download import RichTextDelegate
from calibre.gui2.store.config.chooser.models import Matches
class ResultsView(QTreeView):
def __init__(self, *args):
QTreeView.__init__(self,*args)
self._model = Matches([p for p in store_plugins()])
self.setModel(self._model)
self.setIconSize(QSize(24, 24))
self.rt_delegate = RichTextDelegate(self)
for i in self._model.HTML_COLS:
self.setItemDelegateForColumn(i, self.rt_delegate)
for i in xrange(self._model.columnCount()):
self.resizeColumnToContents(i)
self.model().sort(1, Qt.AscendingOrder)
self.header().setSortIndicator(self.model().sort_col, self.model().sort_order)

View File

@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QWidget from PyQt4.Qt import QWidget
from calibre.gui2 import JSONConfig from calibre.gui2 import JSONConfig
from calibre.gui2.store.config.search_widget_ui import Ui_Form from calibre.gui2.store.config.search.search_widget_ui import Ui_Form
class StoreConfigWidget(QWidget, Ui_Form): class StoreConfigWidget(QWidget, Ui_Form):

View File

@ -93,13 +93,13 @@
<item> <item>
<widget class="QGroupBox" name="groupBox_3"> <widget class="QGroupBox" name="groupBox_3">
<property name="title"> <property name="title">
<string>Performance</string> <string>Threads</string>
</property> </property>
<layout class="QFormLayout" name="formLayout_3"> <layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="text"> <property name="text">
<string>Number of simultaneous searches</string> <string>Number of search threads to use</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -113,7 +113,7 @@
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Number of simultaneous cache updates</string> <string>Number of cache update threads to use</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -127,7 +127,7 @@
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="text"> <property name="text">
<string>Number of simultaneous cover downloads</string> <string>Number of conver download threads to use</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -141,7 +141,7 @@
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_7"> <widget class="QLabel" name="label_7">
<property name="text"> <property name="text">
<string>Number of simultaneous details downloads</string> <string>Number of details threads to use</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -11,7 +11,7 @@ Config widget access functions for configuring the store action.
''' '''
def config_widget(): def config_widget():
from calibre.gui2.store.config.search_widget import StoreConfigWidget from calibre.gui2.store.config.search.search_widget import StoreConfigWidget
return StoreConfigWidget() return StoreConfigWidget()
def save_settings(config_widget): def save_settings(config_widget):

View File

@ -3,3 +3,6 @@ or asked not to be included in the store integration.
* Borders (http://www.borders.com/) * Borders (http://www.borders.com/)
* WH Smith (http://www.whsmith.co.uk/) * WH Smith (http://www.whsmith.co.uk/)
Refused to permit signing up for the affiliate program
* Libraria Rizzoli (http://libreriarizzoli.corriere.it/).
No reply with two attempts over 2 weeks

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, Tomasz Długosz <tomek3d@gmail.com>'
__docformat__ = 'restructuredtext en'
import re
import urllib
from contextlib import closing
from lxml import html
from PyQt4.Qt import QUrl
from calibre import browser, url_slash_cleaner
from calibre.gui2 import open_url
from calibre.gui2.store import StorePlugin
from calibre.gui2.store.basic_config import BasicStoreConfig
from calibre.gui2.store.search_result import SearchResult
from calibre.gui2.store.web_store_dialog import WebStoreDialog
class LegimiStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
url = 'http://www.legimi.com/pl/ebooks/?price=any'
detail_url = None
if detail_item:
detail_url = detail_item
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else:
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):
url = 'http://www.legimi.com/pl/ebooks/?price=any&lang=pl&search=' + urllib.quote_plus(query.encode('utf-8')) + '&sort=relevance'
br = browser()
counter = max_results
with closing(br.open(url, timeout=timeout)) as f:
doc = html.fromstring(f.read())
for data in doc.xpath('//div[@class="list"]/ul/li'):
if counter <= 0:
break
id = ''.join(data.xpath('.//div[@class="item_cover_container"]/a[1]/@href'))
if not id:
continue
cover_url = ''.join(data.xpath('.//div[@class="item_cover_container"]/a/img/@src'))
title = ''.join(data.xpath('.//div[@class="item_entries"]/h2/a/text()'))
author = ''.join(data.xpath('.//div[@class="item_entries"]/span[1]/a/text()'))
price = ''.join(data.xpath('.//div[@class="item_entries"]/span[3]/text()'))
price = re.sub(r'[^0-9,]*','',price) + ''
counter -= 1
s = SearchResult()
s.cover_url = 'http://www.legimi.com/' + cover_url
s.title = title.strip()
s.author = author.strip()
s.price = price
s.detail_item = 'http://www.legimi.com/' + id.strip()
s.drm = SearchResult.DRM_LOCKED
s.formats = 'EPUB'
yield s

View File

@ -47,6 +47,7 @@ class BooksModel(QAbstractItemModel):
self.books = list(self.search_filter.parse(self.filter)) self.books = list(self.search_filter.parse(self.filter))
except: except:
self.books = self.all_books self.books = self.all_books
self.layoutChanged.emit()
self.sort(self.sort_col, self.sort_order) self.sort(self.sort_col, self.sort_order)
self.total_changed.emit(self.rowCount()) self.total_changed.emit(self.rowCount())

View File

@ -116,7 +116,7 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
if price: if price:
ans.append('price:"' + self.mc + price + '"') ans.append('price:"' + self.mc + price + '"')
format = unicode(self.format_box.text()).strip() format = unicode(self.format_box.text()).strip()
if author: if format:
ans.append('format:"' + self.mc + format + '"') ans.append('format:"' + self.mc + format + '"')
if ans: if ans:
return ' and '.join(ans) return ' and '.join(ans)

View File

@ -14,7 +14,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox,
from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2 import JSONConfig, info_dialog
from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.store.config.search_widget import StoreConfigWidget from calibre.gui2.store.config.search.search_widget import StoreConfigWidget
from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog
from calibre.gui2.store.search.download_thread import SearchThreadPool, \ from calibre.gui2.store.search.download_thread import SearchThreadPool, \
CacheUpdateThreadPool CacheUpdateThreadPool
@ -238,7 +238,7 @@ class SearchDialog(QDialog, Ui_Dialog):
v = QVBoxLayout(d) v = QVBoxLayout(d)
button_box.accepted.connect(d.accept) button_box.accepted.connect(d.accept)
button_box.rejected.connect(d.reject) button_box.rejected.connect(d.reject)
d.setWindowTitle(_('Customize Get Books')) d.setWindowTitle(_('Customize get books search'))
config_widget = StoreConfigWidget(self.config) config_widget = StoreConfigWidget(self.config)
v.addWidget(config_widget) v.addWidget(config_widget)
v.addWidget(button_box) v.addWidget(button_box)

View File

@ -82,8 +82,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>102</width> <width>125</width>
<height>129</height> <height>127</height>
</rect> </rect>
</property> </property>
</widget> </widget>
@ -159,6 +159,9 @@
<property name="expandsOnDoubleClick"> <property name="expandsOnDoubleClick">
<bool>false</bool> <bool>false</bool>
</property> </property>
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
</widget> </widget>
</item> </item>
<item> <item>

View File

@ -221,7 +221,12 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
if not ip or ip.startswith('127.'): if not ip or ip.startswith('127.'):
raise raise
cherrypy.log('Trying to bind to single interface: '+ip) cherrypy.log('Trying to bind to single interface: '+ip)
# Change the host we listen on
cherrypy.config.update({'server.socket_host' : ip}) cherrypy.config.update({'server.socket_host' : ip})
# This ensures that the change is actually applied
cherrypy.server.socket_host = ip
cherrypy.server.httpserver = cherrypy.server.instance = None
cherrypy.engine.start() cherrypy.engine.start()
self.is_running = True self.is_running = True
@ -231,6 +236,8 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
cherrypy.engine.block() cherrypy.engine.block()
except Exception as e: except Exception as e:
self.exception = e self.exception = e
import traceback
traceback.print_exc()
finally: finally:
self.is_running = False self.is_running = False
try: try: